authyra
Authentication framework for Dart. No Flutter dependency. Runs on mobile, web, desktop, backend (Shelf, Dart Frog), and CLI.
What it does
AuthyraClient— stateless orchestrator. Handles provider registration, session creation, and token operations. No global state, fully injectable, 100% testable.AuthyraInstance— singleton wrapper. Adds reactiveStream<AuthState>, synchronous state cache, and a one-call initialization API for app startup.SessionManager— multi-account session registry. Persists, restores, switches, and cleans up sessions.AuthStorage— pluggable interface. Bring your own persistence:flutter_secure_storage, Redis, encrypted file — anything.AuthProvider— pluggable strategy interface. Implement it to add any authentication flow.
Installation
dependencies:
authyra: ^0.1.0
Quick start
import 'package:authyra/authyra.dart';
void main() async {
await Authyra.initialize(
client: AuthyraClient(
providers: [
CredentialsProvider.withTokens(
id: 'email',
authorize: (creds) async {
final res = await myApi.post('/auth/login', body: creds);
if (res.statusCode != 200) return null;
return AuthSignInResult(
user: AuthUser(id: res.data['id'], email: res.data['email']),
accessToken: res.data['accessToken'],
refreshToken: res.data['refreshToken'],
expiresAt: DateTime.parse(res.data['expiresAt']),
);
},
),
],
storage: MyStorage(), // implement AuthStorage for your platform
),
);
// Sign in
final user = await Authyra.instance.signIn('email', params: {
'email': 'alice@example.com',
'password': 's3cr3t',
});
// Synchronous state — no await
print(Authyra.instance.isAuthenticated); // true
print(Authyra.instance.currentUser?.email);
// Reactive state
Authyra.instance.authStateChanges.listen((state) {
switch (state.type) {
case AuthStateType.authenticated: print('Hello ${state.user!.name}');
case AuthStateType.unauthenticated: print('Signed out');
case AuthStateType.error: print('Error: ${state.error}');
}
});
// Sign out
await Authyra.instance.signOut();
}
Providers
CredentialsProvider — email / password
Two constructors depending on what your backend returns:
// Backend returns a user profile only (cookie / opaque session)
CredentialsProvider(
id: 'email',
authorize: (creds) async {
final res = await myApi.post('/login', body: creds);
if (res.statusCode != 200) return null;
return AuthUser(id: res.data['id'], email: res.data['email']);
},
)
// Backend returns JWT tokens (recommended)
CredentialsProvider.withTokens(
id: 'email',
authorize: (creds) async {
final res = await myApi.post('/login', body: creds);
if (res.statusCode != 200) return null;
return AuthSignInResult(
user: AuthUser(id: res.data['userId'], email: res.data['email']),
accessToken: res.data['accessToken'],
refreshToken: res.data['refreshToken'],
expiresAt: DateTime.parse(res.data['expiresAt']),
);
},
)
Custom provider
Implement AuthProvider to plug in any strategy — SAML, magic link, phone OTP, a proprietary SSO:
class MagicLinkProvider implements AuthProvider {
@override String get id => 'magic-link';
@override AuthProviderType get type => AuthProviderType.magicLink;
@override bool get supportsRefresh => false;
@override
Future<AuthSignInResult?> signIn({Map<String, dynamic>? params}) async {
final token = params?['token'] as String?;
if (token == null) return null;
final res = await myApi.post('/auth/magic', body: {'token': token});
if (res.statusCode != 200) return null;
return AuthSignInResult(
user: AuthUser(id: res.data['id'], email: res.data['email']),
accessToken: res.data['accessToken'],
expiresAt: DateTime.parse(res.data['expiresAt']),
);
}
}
Storage
AuthStorage is a key-value interface. Implement it for your runtime:
class MyRedisStorage implements AuthStorage {
@override Future<void> initialize() async { /* connect */ }
@override Future<String?> read(String key) => redis.get(key);
@override Future<void> write(String key, String value) => redis.set(key, value);
@override Future<bool> delete(String key) async {
final existed = await redis.exists(key) > 0;
await redis.del([key]);
return existed;
}
@override Future<void> clear() => redis.flushDb();
@override Future<bool> containsKey(String key) async => await redis.exists(key) > 0;
@override Future<List<String>> getKeysWithPrefix(String p) => redis.keys('$p*');
}
An InMemoryStorage (non-persistent) is exported from the package for use in tests and development.
Multi-account
Authyra supports multiple signed-in accounts out of the box:
// List all signed-in users
final accounts = await Authyra.instance.accounts.getAll();
// Switch active account
await Authyra.instance.accounts.switchTo(userId);
// Sign out a specific account
await Authyra.instance.accounts.signOut(userId);
// Sign out all accounts
await Authyra.instance.accounts.signOutAll();
// Remove stale expired sessions
final removed = await Authyra.instance.accounts.cleanExpired();
Advanced: using AuthyraClient directly
For dependency injection or backend use cases where you don't want a singleton:
final client = AuthyraClient(
providers: [CredentialsProvider(id: 'email', authorize: myAuthorize)],
storage: MyStorage(),
config: const AuthConfig(autoRefresh: true),
);
await client.initialize();
final user = await client.signIn('email', params: {'email': '...', 'password': '...'});
client.authStateStream.listen((state) { /* react */ });
Testing
import 'package:test/test.dart';
import 'package:authyra/authyra.dart';
void main() {
test('sign in succeeds with valid credentials', () async {
final client = AuthyraClient(
providers: [
CredentialsProvider(
id: 'email',
authorize: (creds) async =>
AuthUser(id: '1', email: creds!['email'] as String),
),
],
storage: InMemoryStorage(),
);
await client.initialize();
final user = await client.signIn('email', params: {
'email': 'alice@example.com',
'password': 'any',
});
expect(user.email, 'alice@example.com');
});
}
Flutter
Use authyra_flutter for Flutter apps. It re-exports this entire package plus adds OAuth2 providers (Google, GitHub, Apple), SecureAuthStorage, AuthGuard widget, and GoRouter integration. One import, everything included.
Documentation
License
MIT