Sessions
AuthSession is the core data object that represents an authenticated user's active session. Token fields aren't stored directly on it: AuthSession is a pointer to the active AuthAccount inside linkedAccounts, which is where tokens actually live.
Fields
class AuthSession {
final AuthUser user;
final String activeAccountId; // AuthAccount.id this session currently uses
final List<AuthAccount> linkedAccounts;
final SessionMetadata? metadata;
final DateTime createdAt;
final DateTime lastUsedAt;
}
class AuthAccount {
final String id; // e.g. 'google_10987654321'
final String userId;
final String providerId; // 'google', 'email', etc.
final String providerAccountId; // the provider's own id for this user
final String? accessToken;
final String? refreshToken;
final DateTime? tokenExpiresAt;
final Map<String, dynamic> providerData;
}
| Field | Description |
|---|---|
user | Identity profile, id, email, name, avatarUrl, metadata |
activeAccountId | The AuthAccount.id currently in use. Token getters (accessToken, refreshToken, expiresAt, providerId) all delegate to this entry via activeAccount |
linkedAccounts | Every provider-linked account entry for this user. activeAccount is the one matching activeAccountId |
metadata | Optional device/network context (ipAddress, userAgent, deviceId, country); never read internally, surfaced for your own audit logs |
createdAt | When this session was first created |
lastUsedAt | Timestamp of the most recent activity, used for recency sorting |
accessToken, refreshToken, expiresAt, and providerId are computed getters that read from activeAccount, null when activeAccountId doesn't match any entry in linkedAccounts.
Lifecycle
signIn()
└─ provider.signIn() returns AuthSignInResult
└─ AuthyraClient builds AuthSession
└─ SessionManager.saveSession(session)
└─ persisted to AuthStorage
└─ authStateChanges emits AuthState.authenticated
↓ (token approaching expiry)
session.shouldRefresh → true
└─ provider.refreshToken(session.refreshToken)
└─ session.refreshed(newAccessToken, newExpiresAt)
└─ SessionManager.updateSession()
└─ authStateChanges emits refreshed state
↓ (sign-out)
signOut()
└─ provider.signOut() [if supportsSignOut]
└─ SessionManager.clearActiveSession()
└─ storage cleared
└─ authStateChanges emits AuthState.unauthenticated
Expiry helpers
isExpired
bool get isExpired
true when DateTime.now() is past expiresAt. Returns false when expiresAt is null.
isExpiringSoon
bool isExpiringSoon([Duration threshold = const Duration(minutes: 5)])
true when the token expires within threshold. Use for manual pre-emptive refresh:
final session = await Authyra.instance.getSession();
if (session?.isExpiringSoon(const Duration(minutes: 10)) ?? false) {
await Authyra.instance.refreshSession();
}
shouldRefresh
bool shouldRefresh({Duration threshold = const Duration(minutes: 5)})
Semantically identical to isExpiringSoon but accepts a named parameter, used internally by AuthyraClient's auto-refresh scheduler:
if (session.shouldRefresh(threshold: config.refreshThreshold)) {
await client.refreshSession();
}
timeUntilExpiration
Duration get timeUntilExpiration
Remaining time until the access token expires. Returns Duration.zero when already expired or when expiresAt is null.
final remaining = session?.timeUntilExpiration ?? Duration.zero;
print('Token valid for ${remaining.inMinutes} more minutes');
Token refresh
When the session should be refreshed:
AuthyraClientcallsprovider.refreshToken(session.refreshToken).- The provider returns an
AuthTokenResult. - The session is updated via
session.refreshed(...). - The new session is persisted and
authStateChangesemits the updated state.
// The refreshed() helper produces an updated AuthSession:
final renewed = session.refreshed(
newAccessToken: result.accessToken,
newRefreshToken: result.refreshToken, // null = keep existing
newExpiresAt: result.expiresAt,
);
When refreshToken returns null (expired or revoked), the session is cleared and AuthState.unauthenticated() is emitted.
canRefresh
bool get canRefresh // refreshToken != null && refreshToken.isNotEmpty
Quick check before attempting a refresh. Providers that don't return a refresh token (e.g., cookie-session flows) will have canRefresh: false.
Reading the session
// Async, full session object
final session = await Authyra.instance.getSession();
print('Provider: ${session?.providerId}');
print('Expires: ${session?.expiresAt}');
print('Can refresh: ${session?.canRefresh}');
// Async, just the access token (triggers auto-refresh if needed)
final token = await Authyra.instance.getAccessToken();
myHttpClient.options.headers['Authorization'] = 'Bearer $token';
Serialisation
AuthSession (and each AuthAccount inside linkedAccounts) is serialised to/from JSON for persistence in AuthStorage. Timestamps are encoded as ISO 8601 strings:
{
"user": { "id": "123", "email": "alice@example.com" },
"activeAccountId": "google_109876543210",
"linkedAccounts": [
{
"id": "google_109876543210",
"userId": "123",
"providerId": "google",
"providerAccountId": "109876543210",
"accessToken": "ya29.xxx",
"refreshToken": "1//yyy",
"tokenExpiresAt": "2026-03-01T12:00:00.000Z",
"providerData": {}
}
],
"metadata": null,
"createdAt": "2026-02-01T08:00:00.000Z",
"lastUsedAt": "2026-02-23T10:30:00.000Z"
}
Each account is persisted under its own session:{userId} storage key (plus a small index for the active account), not as one combined blob, so refreshing or removing one account never rewrites another account's entry.
SecureAuthStorage in Flutter, or your own encrypted implementation for other runtimes.Multi-account sessions
SessionRegistry maintains an in-memory map of active sessions keyed by user.id. AccountManager exposes the public API:
final accounts = Authyra.instance.accounts;
// All signed-in accounts, sorted by last activity
final users = await accounts.getAll();
// Activate a different account
await accounts.switchTo(userId);
// Sign out one account, keep others active
await accounts.signOut(userId);
// Sign out all accounts
await accounts.signOutAll();
// Remove expired sessions
await accounts.cleanExpired();
There is no hard cap on the number of concurrent accounts.
linkedAccounts and hasLinkedProvider
GoogleProvider, GitHubProvider, etc.) populates linkedAccounts with more than one entry: each derives AuthUser.id from that provider's own subject claim, so signing in via two different providers registers as two separate accounts in SessionRegistry, not one linked identity. linkedAccounts is real, tested infrastructure for account-linking, but the linking flow itself (matching a new provider sign-in to an existing user) isn't built yet.linkedProviderIds is a convenience getter over linkedAccounts:
final session = await Authyra.instance.getSession();
final providers = session?.linkedProviderIds ?? <String>{};
if (session?.hasLinkedProvider('github') ?? false) {
showGitHubBadge();
}