+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
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:
@@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user