Files
sendico/frontend/pshared/lib/service/authorization/token.dart
Stephan D c6a56071b5
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/chain_gateway Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/bump_version Pipeline failed
+signup +login
2025-11-17 20:16:45 +01:00

180 lines
6.4 KiB
Dart

import 'package:logging/logging.dart';
import 'package:pshared/api/errors/authorization_failed.dart';
import 'package:pshared/api/errors/unauthorized.dart';
import 'package:pshared/api/requests/tokens/access_refresh.dart';
import 'package:pshared/api/requests/tokens/refresh_rotate.dart';
import 'package:pshared/api/responses/account.dart';
import 'package:pshared/api/responses/error/server.dart';
import 'package:pshared/api/responses/login.dart';
import 'package:pshared/api/responses/token.dart';
import 'package:pshared/config/constants.dart';
import 'package:pshared/service/authorization/circuit_breaker.dart';
import 'package:pshared/service/authorization/retry_helper.dart';
import 'package:pshared/service/authorization/storage.dart';
import 'package:pshared/service/authorization/token_mutex.dart';
import 'package:pshared/service/device_id.dart';
import 'package:pshared/service/services.dart';
import 'package:pshared/utils/exception.dart';
import 'package:pshared/utils/http/requests.dart';
class TokenService {
static final _logger = Logger('service.authorization.token');
static const String _objectType = Services.account;
static bool _isTokenExpiringSoon(TokenData token, Duration duration) {
return token.expiration.isBefore(DateTime.now().add(duration));
}
static Future<String> getAccessToken() async {
TokenData token = await AuthorizationStorage.getAccessToken();
if (_isTokenExpiringSoon(token, const Duration(hours: 4))) {
// Use mutex to prevent concurrent refresh operations
final refreshedToken = await TokenRefreshMutex().executeRefresh(() async {
return (await refreshAccessToken()).accessToken.token;
});
return refreshedToken;
}
return token.token;
}
static Future<void> _updateTokens(LoginResponse response) async {
await AuthorizationStorage.updateToken(response.accessToken);
await AuthorizationStorage.updateRefreshToken(response.refreshToken);
}
static Future<AccountResponse> refreshAccessToken() async {
_logger.fine('Refreshing access token...');
final deviceId = await DeviceIdManager.getDeviceId();
final refresh = await AuthorizationStorage.getRefreshToken();
if (_isTokenExpiringSoon(refresh, const Duration(days: 7))) {
return await _rotateRefreshToken();
}
final response = await getPOSTResponse(
_objectType,
'/refresh',
AccessTokenRefreshRequest(
deviceId: deviceId,
clientId: Constants.clientId,
token: refresh.token,
).toJson(),
);
final accountResp = AccountResponse.fromJson(response);
await AuthorizationStorage.updateToken(accountResp.accessToken);
return accountResp;
}
static Future<LoginResponse> _rotateRefreshToken() async {
_logger.fine('Rotating refresh token...');
final refresh = await AuthorizationStorage.getRefreshToken();
if (refresh.expiration.isBefore(DateTime.now())) throw ErrorUnauthorized();
final deviceId = await DeviceIdManager.getDeviceId();
final response = await getPOSTResponse(
_objectType,
'/rotate',
RotateRefreshTokenRequest(
token: refresh.token,
clientId: Constants.clientId,
deviceId: deviceId,
).toJson(),
);
final loginResponse = LoginResponse.fromJson(response);
await _updateTokens(loginResponse);
return loginResponse;
}
/// Enhanced method to handle unexpected 401 errors with fallback logic
static Future<void> handleUnauthorized() async {
_logger.warning('Handling unexpected 401 unauthorized error');
return AuthCircuitBreaker.execute(() async {
return RetryHelper.withExponentialBackoff(
() async {
try {
// Try refresh first (faster)
final currentRefresh = await AuthorizationStorage.getRefreshToken();
if (!_isTokenExpiringSoon(currentRefresh, const Duration(days: 1))) {
_logger.fine('Attempting access token refresh for 401 recovery');
await TokenRefreshMutex().executeRefresh(() async {
await refreshAccessToken();
return 'refreshed';
});
return;
}
// Fallback to rotation if refresh token expiring soon
_logger.fine('Attempting refresh token rotation for 401 recovery');
await TokenRefreshMutex().executeRotation(() async {
await _rotateRefreshToken();
});
} catch (e) {
_logger.severe('Token recovery failed: $e');
throw AuthenticationFailedException('Token recovery failed', toException(e));
}
},
maxRetries: 2,
shouldRetry: (error) {
// Only retry on network errors, not auth errors
return RetryHelper.isRetryableError(error) && !_isAuthError(error);
},
);
});
}
/// Enhanced getAccessToken with better error handling
static Future<String> getAccessTokenSafe() async {
return AuthCircuitBreaker.execute(() async {
return RetryHelper.withExponentialBackoff(
() async {
TokenData token = await AuthorizationStorage.getAccessToken();
if (_isTokenExpiringSoon(token, const Duration(hours: 4))) {
// Use mutex to prevent concurrent refresh operations
final refreshedToken = await TokenRefreshMutex().executeRefresh(() async {
return (await refreshAccessToken()).accessToken.token;
});
return refreshedToken;
}
return token.token;
},
maxRetries: 2,
shouldRetry: (error) => RetryHelper.isRetryableError(error),
);
});
}
/// Check if error is authentication-related (non-retryable)
static bool _isAuthError(Exception error) {
if (error is ErrorUnauthorized || error is AuthenticationFailedException) {
return true;
}
if (error is ErrorResponse && error.code == 401) {
return true;
}
final errorString = error.toString().toLowerCase();
return errorString.contains('unauthorized') ||
errorString.contains('401') ||
errorString.contains('authentication') ||
errorString.contains('token');
}
/// Get circuit breaker status for debugging
static Map<String, dynamic> getAuthStatus() {
return {
'circuitBreaker': AuthCircuitBreaker.getStatus(),
'tokenMutex': TokenRefreshMutex().getStatus(),
'timestamp': DateTime.now().toIso8601String(),
};
}
}