Core Concepts
Providers
The AuthProvider interface, strategy types, sign-in, sign-out, and token refresh.
Every authentication strategy in Authyra implements the AuthProvider interface. AuthyraClient registers providers by id and delegates signIn, signOut, and refreshToken to the matching provider at runtime.
AuthProvider interface
abstract class AuthProvider {
String get id; // unique slug, e.g. 'google'
String get name => id; // display name for logs/UI
bool get supportsRefresh => false;
bool get supportsSignOut => false;
Future<AuthSignInResult?> signIn({AuthSignInParams? params});
Future<void> signOut({String? userId}) async {}
Future<AuthTokenResult?> refreshToken(String refreshToken) async => null;
}
| Member | Required | Description |
|---|---|---|
id | yes | Unique lowercase slug within an AuthyraClient |
name | no | Human-readable label, defaults to id |
supportsRefresh | no | Opt-in to silent token renewal. Default: false |
supportsSignOut | no | Opt-in to server-side revocation on sign-out. Default: false |
signIn | yes | Authenticate the user; return null on credential failure |
signOut | no | Revoke server-side credentials, only called when supportsSignOut: true |
refreshToken | no | Exchange a refresh token for new tokens, only called when supportsRefresh: true |
There's no
MagicLinkSignInParams or PhoneSignInParams shipped yet. Build a passwordless or OTP flow today by extending AuthSignInParams yourself and implementing AuthProvider directly, see Custom Provider →.Result types
AuthSignInResult
Returned by signIn. Carries the user profile and any tokens the provider received.
class AuthSignInResult {
final AuthUser user;
final AuthAccount? account; // omit it, AuthyraClient builds a minimal one
final String? accessToken;
final String? refreshToken;
final DateTime? expiresAt;
}
Tokens are optional, omit them for server-side session flows (cookie auth) where tokens are managed opaquely by the backend. When present, AuthyraClient stores them on the session's active AuthAccount, enabling getAccessToken() and silent refresh.
accessToken/refreshToken must be application session tokens, never a raw identity-provider credential that still needs exchanging (a Google idToken, an Apple identityToken). A provider that only acquires such a credential should put it in AuthAccount.providerData instead and leave these fields null. See authyra_apple's AppleProvider for an example built around exactly this distinction.AuthTokenResult
Returned by refreshToken. Token-only, the existing user profile is preserved.
class AuthTokenResult {
final String accessToken;
final String? refreshToken; // null = keep existing refresh token
final DateTime expiresAt;
}
Built-in providers
| Provider | Package | Strategy | PKCE | Client secret |
|---|---|---|---|---|
CredentialsProvider | authyra | Form-based | n/a | n/a |
CredentialsProvider.withTokens | authyra | Form-based | n/a | n/a |
OAuth2Provider | authyra_flutter | Browser OAuth2 | ✓ (default) | optional |
GoogleProvider | authyra_flutter | Browser OAuth2 | ✓ | optional |
GitHubProvider | authyra_flutter | Browser OAuth2 | ✗ | required |
ProxyOAuthProvider | authyra_flutter | Backend-delegated | n/a | server-side |
AppleProvider | authyra_apple | Native SDK | n/a | none needed in-app |
Implementing a custom provider
Implement AuthProvider directly for any strategy Authyra doesn't ship:
class MyApiProvider implements AuthProvider {
@override
String get id => 'my-api';
@override
bool get supportsRefresh => true;
@override
bool get supportsSignOut => true;
@override
Future<AuthSignInResult?> signIn({AuthSignInParams? params}) async {
final creds = params is CredentialsSignInParams ? params : null;
final res = await myApi.post('/auth/login', body: {
'email': creds?.email,
'password': creds?.password,
});
if (res.statusCode == 401) return null; // wrong credentials
if (res.statusCode != 200) throw AuthenticationFailedException(
'Login failed: HTTP ${res.statusCode}',
providerName: id,
);
return AuthSignInResult(
user: AuthUser(
id: res.data['id'],
email: res.data['email'],
name: res.data['name'],
),
accessToken: res.data['accessToken'],
refreshToken: res.data['refreshToken'],
expiresAt: DateTime.parse(res.data['expiresAt']),
);
}
@override
Future<AuthTokenResult?> refreshToken(String refreshToken) async {
final res = await myApi.post('/auth/refresh',
body: {'refreshToken': refreshToken});
if (res.statusCode != 200) return null;
return AuthTokenResult(
accessToken: res.data['accessToken'],
expiresAt: DateTime.parse(res.data['expiresAt']),
// refreshToken omitted → client keeps the existing one
);
}
@override
Future<void> signOut({String? userId}) async {
// Fire-and-forget, never throw here
await myApi.post('/auth/logout').catchError((_) {});
}
}
Register it in AuthyraClient like any other provider:
AuthyraClient(
providers: [MyApiProvider()],
storage: SecureAuthStorage(),
)
Provider design rules
- Stateless: providers hold configuration, never session state. Session state lives in
SessionManager. - Return
nullon credential failure:AuthyraClientconverts anullreturn fromsignInintoAuthenticationFailedException. - Throw for infrastructure errors: network failures, CSRF mismatches, and invalid config should throw an
AuthyraExceptionsubclass, not returnnull. signOutmust never throw: catch and log errors internally to avoid blocking local session cleanup.- Populate token fields when available: leaving
accessTokenasnullpreventsgetAccessToken()and silent refresh from working.