+signup +login
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

This commit is contained in:
Stephan D
2025-11-17 20:16:45 +01:00
parent 1ab7f2e7d3
commit c6a56071b5
89 changed files with 1308 additions and 3497 deletions

View File

@@ -1,20 +1,25 @@
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;
@@ -26,7 +31,11 @@ class TokenService {
static Future<String> getAccessToken() async {
TokenData token = await AuthorizationStorage.getAccessToken();
if (_isTokenExpiringSoon(token, const Duration(hours: 4))) {
token = (await _refreshAccessToken()).accessToken;
// Use mutex to prevent concurrent refresh operations
final refreshedToken = await TokenRefreshMutex().executeRefresh(() async {
return (await refreshAccessToken()).accessToken.token;
});
return refreshedToken;
}
return token.token;
}
@@ -36,13 +45,13 @@ class TokenService {
await AuthorizationStorage.updateRefreshToken(response.refreshToken);
}
static Future<AccountResponse> _refreshAccessToken() async {
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();
return await _rotateRefreshToken();
}
final response = await getPOSTResponse(
@@ -60,7 +69,7 @@ class TokenService {
return accountResp;
}
static Future<LoginResponse> rotateRefreshToken() async {
static Future<LoginResponse> _rotateRefreshToken() async {
_logger.fine('Rotating refresh token...');
final refresh = await AuthorizationStorage.getRefreshToken();
@@ -82,4 +91,89 @@ class TokenService {
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(),
};
}
}