The Token Management system provides secure, automatic handling of authentication tokens with support for refresh strategies and flexible configuration.
- Quick Start
- Token Manager
- Token Configuration
- Refresh Strategies
- Token Interceptor
- Best Practices
- Examples
import 'package:flutter_infra/flutter_infra.dart';
// Create storage service
final storageService = await StorageService.create();
// Create token manager with default configuration
final tokenManager = DefaultTokenManager(storage: storageService);
// Save tokens
await tokenManager.saveToken('your_access_token');
await tokenManager.saveRefreshToken('your_refresh_token');
// Retrieve tokens
final accessToken = await tokenManager.getToken();
final refreshToken = await tokenManager.getRefreshToken();
// Create network service with automatic token injection
final networkService = await NetworkService.createWithTokenSupport(
config: NetworkConfig(baseUrl: 'https://api.example.com'),
tokenManager: tokenManager,
);// Create a refresh strategy
final refreshStrategy = CustomRefreshStrategy(
networkService: networkService,
tokenManager: tokenManager,
);
// Create network service with token refresh support
final networkService = await NetworkService.createWithTokenSupport(
config: NetworkConfig(baseUrl: 'https://api.example.com'),
tokenManager: tokenManager,
refreshStrategy: refreshStrategy,
);
// Tokens will automatically refresh on 401 errors
final response = await networkService.get('/protected-resource');The default implementation stores tokens securely and provides basic token management:
final tokenManager = DefaultTokenManager(
storage: storageService,
config: TokenConfig(
tokenStorageKey: 'access_token',
refreshTokenStorageKey: 'refresh_token',
tokenHeaderKey: 'Authorization',
tokenPrefix: 'Bearer',
),
);You can implement your own token manager for specialized needs:
class CustomTokenManager implements TokenManager {
final StorageService _storage;
final TokenConfig _config;
CustomTokenManager(this._storage, this._config);
@override
Future<String?> getToken() async {
// Custom logic for token retrieval
final token = await _storage.getSecureString(_config.tokenStorageKey);
// Add custom validation, decryption, etc.
if (token != null && _isTokenValid(token)) {
return token;
}
return null;
}
@override
Future<void> saveToken(String token) async {
// Custom logic for token storage
await _storage.setSecureString(_config.tokenStorageKey, token);
await _storage.setSecureDateTime('token_saved_at', DateTime.now());
}
@override
Future<void> deleteToken() async {
await _storage.deleteSecureKey(_config.tokenStorageKey);
await _storage.deleteSecureKey('token_saved_at');
}
// Implement other required methods...
}final tokenConfig = TokenConfig(
// HTTP Header Configuration
tokenHeaderKey: 'Authorization', // Header name for token
tokenPrefix: 'Bearer', // Prefix for token value
// Storage Configuration
tokenStorageKey: 'access_token', // Storage key for access token
refreshTokenStorageKey: 'refresh_token', // Storage key for refresh token
// API Response Configuration
tokenResponseField: 'access_token', // Field name in API response
refreshTokenResponseField: 'refresh_token', // Refresh token field in response
// Refresh Configuration
refreshTokenEndPoint: '/auth/refresh', // Endpoint for token refresh
);class TokenConfigs {
static TokenConfig development() => TokenConfig(
tokenHeaderKey: 'Authorization',
tokenPrefix: 'Bearer',
tokenStorageKey: 'dev_access_token',
refreshTokenStorageKey: 'dev_refresh_token',
refreshTokenEndPoint: '/auth/refresh',
);
static TokenConfig staging() => TokenConfig(
tokenHeaderKey: 'X-Auth-Token',
tokenPrefix: 'Token',
tokenStorageKey: 'staging_access_token',
refreshTokenStorageKey: 'staging_refresh_token',
refreshTokenEndPoint: '/api/v1/auth/refresh',
);
static TokenConfig production() => TokenConfig(
tokenHeaderKey: 'Authorization',
tokenPrefix: 'Bearer',
tokenStorageKey: 'prod_access_token',
refreshTokenStorageKey: 'prod_refresh_token',
refreshTokenEndPoint: '/api/auth/refresh',
);
}// For APIs that use custom authentication headers
final customTokenConfig = TokenConfig(
tokenHeaderKey: 'X-API-Key',
tokenPrefix: '', // No prefix
tokenStorageKey: 'api_key',
// No refresh token needed for API keys
);
// For APIs with multiple authentication methods
final multiAuthConfig = TokenConfig(
tokenHeaderKey: 'X-Access-Token',
tokenPrefix: 'Custom',
tokenStorageKey: 'custom_access_token',
refreshTokenStorageKey: 'custom_refresh_token',
tokenResponseField: 'accessToken',
refreshTokenResponseField: 'refreshToken',
refreshTokenEndPoint: '/oauth/token',
);The built-in refresh strategy that works with most OAuth2-style APIs:
final refreshStrategy = DefaultTokenRefreshStrategy(
networkService: networkService,
tokenManager: tokenManager,
);
// Will automatically:
// 1. Use the refresh token to get new tokens
// 2. Save the new tokens
// 3. Retry the original requestImplement your own refresh logic for custom authentication flows:
class CustomRefreshStrategy implements TokenRefreshStrategy {
final NetworkService _networkService;
final TokenManager _tokenManager;
final String _clientId;
final String _clientSecret;
CustomRefreshStrategy({
required NetworkService networkService,
required TokenManager tokenManager,
required String clientId,
required String clientSecret,
}) : _networkService = networkService,
_tokenManager = tokenManager,
_clientId = clientId,
_clientSecret = clientSecret;
@override
Future<bool> refreshToken() async {
try {
final refreshToken = await _tokenManager.getRefreshToken();
if (refreshToken == null) return false;
final response = await _networkService.post('/oauth/token', data: {
'grant_type': 'refresh_token',
'refresh_token': refreshToken,
'client_id': _clientId,
'client_secret': _clientSecret,
});
if (response.isSuccess && response.data is Map) {
final data = response.data as Map;
final newAccessToken = data['access_token'];
final newRefreshToken = data['refresh_token'];
if (newAccessToken != null) {
await _tokenManager.saveToken(newAccessToken);
// Some APIs return a new refresh token
if (newRefreshToken != null) {
await _tokenManager.saveRefreshToken(newRefreshToken);
}
return true;
}
}
return false;
} catch (e) {
print('Token refresh failed: $e');
return false;
}
}
}class AdvancedRefreshStrategy implements TokenRefreshStrategy {
final NetworkService _networkService;
final TokenManager _tokenManager;
final int _maxRetries;
final Duration _retryDelay;
AdvancedRefreshStrategy({
required NetworkService networkService,
required TokenManager tokenManager,
int maxRetries = 3,
Duration retryDelay = const Duration(seconds: 2),
}) : _networkService = networkService,
_tokenManager = tokenManager,
_maxRetries = maxRetries,
_retryDelay = retryDelay;
@override
Future<bool> refreshToken() async {
for (int attempt = 1; attempt <= _maxRetries; attempt++) {
try {
final success = await _attemptRefresh();
if (success) return true;
if (attempt < _maxRetries) {
await Future.delayed(_retryDelay * attempt);
}
} catch (e) {
print('Refresh attempt $attempt failed: $e');
if (attempt == _maxRetries) {
// Clean up tokens on final failure
await _tokenManager.deleteToken();
await _tokenManager.deleteRefreshToken();
return false;
}
}
}
return false;
}
Future<bool> _attemptRefresh() async {
// Implementation similar to CustomRefreshStrategy
// but with additional error handling and validation
}
}The TokenInterceptor automatically handles token injection and refresh:
final tokenInterceptor = TokenInterceptor(
tokenManager: tokenManager,
refreshStrategy: refreshStrategy, // Optional
);
final networkService = await NetworkService.create(
config: NetworkConfig(
baseUrl: 'https://api.example.com',
interceptors: [
LoggerInterceptor(),
tokenInterceptor,
],
),
);- Request Interception: Automatically adds tokens to outgoing requests
- Response Handling: Extracts new tokens from successful responses
- Error Handling: Attempts token refresh on 401 errors
For cases where you need more control:
class ManualTokenInterceptor implements NetworkInterceptor {
final TokenManager _tokenManager;
ManualTokenInterceptor(this._tokenManager);
@override
Future<void> onRequest(NetworkRequest request) async {
// Only add token to specific endpoints
if (request.path.startsWith('/api/')) {
final token = await _tokenManager.getToken();
if (token != null) {
request.headers['Authorization'] = 'Bearer $token';
}
}
}
@override
Future<void> onResponse(NetworkResponse response) async {
// Custom response handling
if (response.statusCode == 200 && response.data is Map) {
final data = response.data as Map;
// Check for new tokens in specific responses
if (data.containsKey('access_token')) {
await _tokenManager.saveToken(data['access_token']);
}
}
}
@override
Future<void> onError(NetworkError error) async {
// Custom error handling
if (error.code == 401) {
// Clear tokens and redirect to login
await _tokenManager.deleteToken();
await _tokenManager.deleteRefreshToken();
// Trigger app-wide logout
NavigationService.redirectToLogin();
}
}
}// ✅ Good: Always use secure storage for tokens
final tokenManager = DefaultTokenManager(
storage: await StorageService.create(),
config: TokenConfig(/* ... */),
);
// ❌ Bad: Never store tokens in normal storage
// await storageService.setString('token', token); // Security risk!class ValidatingTokenManager extends DefaultTokenManager {
ValidatingTokenManager(super.storage, {super.config});
@override
Future<String?> getToken() async {
final token = await super.getToken();
if (token != null && _isTokenExpired(token)) {
await deleteToken();
return null;
}
return token;
}
bool _isTokenExpired(String token) {
try {
// Parse JWT token and check expiration
final parts = token.split('.');
if (parts.length != 3) return true;
final payload = json.decode(
utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))),
);
final exp = payload['exp'] as int?;
if (exp == null) return false;
final expirationDate = DateTime.fromMillisecondsSinceEpoch(exp * 1000);
return DateTime.now().isAfter(expirationDate);
} catch (e) {
return true; // Assume expired if parsing fails
}
}
}class GracefulRefreshStrategy implements TokenRefreshStrategy {
final TokenRefreshStrategy _primaryStrategy;
final VoidCallback _onRefreshFailure;
GracefulRefreshStrategy(this._primaryStrategy, this._onRefreshFailure);
@override
Future<bool> refreshToken() async {
final success = await _primaryStrategy.refreshToken();
if (!success) {
// Notify the app that refresh failed
_onRefreshFailure();
}
return success;
}
}
// Usage
final refreshStrategy = GracefulRefreshStrategy(
DefaultTokenRefreshStrategy(networkService: networkService, tokenManager: tokenManager),
() {
// Handle refresh failure (redirect to login, show error, etc.)
Navigator.pushNamedAndRemoveUntil(context, '/login', (route) => false);
},
);class TokenManagerFactory {
static Future<TokenManager> create(Environment env) async {
final storage = await StorageService.create();
switch (env) {
case Environment.development:
return DefaultTokenManager(
storage: storage,
config: TokenConfigs.development(),
);
case Environment.staging:
return DefaultTokenManager(
storage: storage,
config: TokenConfigs.staging(),
);
case Environment.production:
return ValidatingTokenManager(
storage,
config: TokenConfigs.production(),
);
}
}
}class MonitoredTokenManager implements TokenManager {
final TokenManager _delegate;
final Analytics _analytics;
MonitoredTokenManager(this._delegate, this._analytics);
@override
Future<void> saveToken(String token) async {
await _delegate.saveToken(token);
_analytics.track('token_saved');
}
@override
Future<void> deleteToken() async {
await _delegate.deleteToken();
_analytics.track('token_deleted');
}
@override
Future<String?> getToken() async {
final token = await _delegate.getToken();
if (token == null) {
_analytics.track('token_missing');
}
return token;
}
// Delegate other methods...
}class AuthService {
final NetworkService _networkService;
final TokenManager _tokenManager;
AuthService(this._networkService, this._tokenManager);
Future<LoginResult> login(String email, String password) async {
try {
final response = await _networkService.post('/auth/login', data: {
'email': email,
'password': password,
});
if (response.isSuccess && response.data is Map) {
final data = response.data as Map;
final accessToken = data['access_token'] as String?;
final refreshToken = data['refresh_token'] as String?;
if (accessToken != null) {
await _tokenManager.saveToken(accessToken);
if (refreshToken != null) {
await _tokenManager.saveRefreshToken(refreshToken);
}
return LoginResult.success();
}
}
return LoginResult.failure('Invalid credentials');
} catch (e) {
return LoginResult.failure('Login failed: $e');
}
}
Future<void> logout() async {
try {
// Notify server about logout
await _networkService.post('/auth/logout');
} catch (e) {
// Continue with local logout even if server request fails
print('Server logout failed: $e');
} finally {
// Always clear local tokens
await _tokenManager.deleteToken();
await _tokenManager.deleteRefreshToken();
}
}
Future<bool> isLoggedIn() async {
final token = await _tokenManager.getToken();
return token != null;
}
}class AuthenticatedApiClient {
final NetworkService _networkService;
final TokenManager _tokenManager;
final StreamController<AuthState> _authStateController;
AuthenticatedApiClient(this._networkService, this._tokenManager)
: _authStateController = StreamController<AuthState>.broadcast();
Stream<AuthState> get authStateStream => _authStateController.stream;
Future<T> authenticatedRequest<T>(
Future<NetworkResponse> request,
T Function(dynamic) parser,
) async {
try {
_authStateController.add(AuthState.loading);
final response = await request;
if (response.isSuccess) {
_authStateController.add(AuthState.authenticated);
return parser(response.data);
} else if (response.statusCode == 401) {
// Token might be expired, try refresh
_authStateController.add(AuthState.refreshing);
final refreshed = await _refreshToken();
if (refreshed) {
// Retry original request
final retryResponse = await request;
if (retryResponse.isSuccess) {
_authStateController.add(AuthState.authenticated);
return parser(retryResponse.data);
}
}
_authStateController.add(AuthState.unauthenticated);
throw AuthenticationException('Authentication failed');
} else {
throw ApiException('Request failed: ${response.error?.message}');
}
} catch (e) {
_authStateController.add(AuthState.error);
rethrow;
}
}
Future<bool> _refreshToken() async {
try {
final refreshToken = await _tokenManager.getRefreshToken();
if (refreshToken == null) return false;
final response = await _networkService.post('/auth/refresh', data: {
'refresh_token': refreshToken,
});
if (response.isSuccess && response.data is Map) {
final data = response.data as Map;
final newAccessToken = data['access_token'];
if (newAccessToken != null) {
await _tokenManager.saveToken(newAccessToken);
return true;
}
}
return false;
} catch (e) {
return false;
}
}
}
enum AuthState {
authenticated,
unauthenticated,
loading,
refreshing,
error,
}