Token Refresh
Authyra can renew an expiring access token without prompting the user, as long as the active provider opts in with supportsRefresh: true and returns a refresh token.
Enabling it
AuthyraClient(
providers: [...],
storage: SecureAuthStorage(),
config: const AuthConfig(
autoRefresh: true, // default
refreshBeforeExpiry: 300, // trigger 5 minutes before expiry
),
)
With autoRefresh: true, two things happen automatically:
- Background scheduler: a periodic timer checks the active session and refreshes it proactively when it's within
refreshBeforeExpiryof expiring. - Read-time refresh:
getSession()(and by extensiongetAccessToken()) refreshes inline if the cached session is expiring soon, before returning.
Disable it (autoRefresh: false) in backend or CLI contexts with no continuously running event loop, and manage renewal explicitly.
Manual refresh
final ok = await Authyra.instance.refreshSession();
if (!ok) {
// The session has already been cleared; send the user back to login.
await Authyra.instance.signOut();
}
Retry policy
Internally, TokenRefresher retries a transient failure (the provider's refreshToken() returns null or throws) up to maxRetries additional times (default 2) with retryDelay between attempts (default 5s) before giving up.
Not every failure is transient. TokenRefresher distinguishes:
| Signal | Meaning | Behaviour |
|---|---|---|
refreshToken() returns null | The refresh token itself was rejected by the server | Retried, might succeed on a later attempt (clock skew, transient 5xx) |
Throws RefreshDeniedException | The refresh must never be retried: no refresh token, provider doesn't support refresh, or an AuthCallbacks.onBeforeTokenRefresh override denied it | Session expires immediately, no retries, no delay |
You won't throw RefreshDeniedException yourself in ordinary usage, AuthyraClient does it internally for the three permanent cases. It matters if you're reading TokenRefreshEvent/SessionExpiredEvent timing, or implementing your own RefreshProvider at the SessionManager level.
What happens on exhaustion
When retries are exhausted (or a RefreshDeniedException short-circuits them):
- The session is removed from the registry.
SessionExpiredEventis emitted onclient.events.TokenRefreshEvent(success: false)is emitted.authStateChangesemitsAuthState.unauthenticated().
client.events.on<SessionExpiredEvent>((e) {
showSnackBar('Your session expired. Please sign in again.');
});
Gating refresh with a callback
Override AuthCallbacks.onBeforeTokenRefresh to check a server-side revocation list or enforce your own rate limit:
class RevocationAwareCallbacks extends AuthCallbacks {
@override
Future<CallbackResult> onBeforeTokenRefresh(
AuthUser user,
String refreshToken,
) async {
if (await revocationStore.isRevoked(refreshToken)) {
return const CallbackResult.deny('Refresh token revoked');
}
return const CallbackResult.allow();
}
}
A denial here expires the session immediately (see the table above), it does not get retried.
Implementing refresh in a custom provider
class MyApiProvider implements AuthProvider {
@override bool get supportsRefresh => true;
@override
Future<AuthTokenResult?> refreshToken(String refreshToken) async {
final res = await myApi.post('/auth/refresh', body: {'refreshToken': refreshToken});
if (res.statusCode != 200) return null; // rejected, treated as transient
return AuthTokenResult(
accessToken: res.data['accessToken'],
expiresAt: DateTime.parse(res.data['expiresAt']),
// refreshToken omitted: Authyra keeps the existing one
);
}
}