Fix mobile app force logout with multiples request in onError queue
This commit is contained in:
@@ -49,11 +49,11 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
|
||||
_token = newToken;
|
||||
_configOIDC = newConfig;
|
||||
_authenticationType = AuthenticationType.oidc;
|
||||
log('AuthorizationInterceptors::setTokenAndAuthorityOidc: TOKEN_INITIAL = $newToken');
|
||||
log('AuthorizationInterceptors::setTokenAndAuthorityOidc: INITIAL_TOKEN = ${newToken?.token} | EXPIRED_TIME = ${newToken?.expiredTime}');
|
||||
}
|
||||
|
||||
void _updateNewToken(TokenOIDC newToken) {
|
||||
log('AuthorizationInterceptors::_updateNewToken: NEW_TOKEN = $newToken');
|
||||
log('AuthorizationInterceptors::_updateNewToken: NEW_TOKEN = ${newToken.token} | EXPIRED_TIME = ${newToken.expiredTime}');
|
||||
_token = newToken;
|
||||
}
|
||||
|
||||
@@ -83,13 +83,17 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
|
||||
|
||||
@override
|
||||
void onError(DioError err, ErrorInterceptorHandler handler) async {
|
||||
logError('AuthorizationInterceptors::onError(): DIO_ERROR = $err | METHOD = ${err.requestOptions.method}');
|
||||
logError('AuthorizationInterceptors::onError(): TOKEN = ${_token?.expiredTime} | DIO_ERROR = $err | METHOD = ${err.requestOptions.method}');
|
||||
try {
|
||||
if (validateToRefreshToken(responseStatusCode: err.response?.statusCode)) {
|
||||
log('AuthorizationInterceptors::onError:_validateToRefreshToken');
|
||||
final requestOptions = err.requestOptions;
|
||||
final extraInRequest = requestOptions.extra;
|
||||
final requestOptions = err.requestOptions;
|
||||
final extraInRequest = requestOptions.extra;
|
||||
bool isRetryRequest = false;
|
||||
|
||||
if (validateToRefreshToken(
|
||||
responseStatusCode: err.response?.statusCode,
|
||||
tokenOIDC: _token
|
||||
)) {
|
||||
log('AuthorizationInterceptors::onError:_validateToRefreshToken');
|
||||
final newTokenOidc = PlatformInfo.isIOS
|
||||
? await _handleRefreshTokenOnIOSPlatform()
|
||||
: await _handleRefreshTokenOnOtherPlatform();
|
||||
@@ -101,6 +105,18 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
|
||||
|
||||
_updateNewToken(newTokenOidc);
|
||||
|
||||
isRetryRequest = true;
|
||||
} else if (validateToRetryTheRequestWithNewToken(
|
||||
authHeader: requestOptions.headers[HttpHeaders.authorizationHeader],
|
||||
tokenOIDC: _token
|
||||
)) {
|
||||
log('AuthorizationInterceptors::onError:validateToRetryTheRequestWithNewToken');
|
||||
isRetryRequest = true;
|
||||
} else {
|
||||
return super.onError(err, handler);
|
||||
}
|
||||
|
||||
if (isRetryRequest) {
|
||||
if (extraInRequest.containsKey(FileUploader.uploadAttachmentExtraKey)) {
|
||||
log('AuthorizationInterceptors::onError: Perform upload attachment request');
|
||||
final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey];
|
||||
@@ -147,24 +163,33 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
|
||||
}
|
||||
}
|
||||
|
||||
bool _isTokenExpired() => _token?.isExpired == true;
|
||||
bool _isTokenExpired(TokenOIDC? tokenOIDC) => tokenOIDC?.isExpired == true;
|
||||
|
||||
bool _isAuthenticationOidcValid() => _authenticationType == AuthenticationType.oidc && _configOIDC != null;
|
||||
|
||||
bool _isTokenNotEmpty() => _token?.token.isNotEmpty == true;
|
||||
bool _isTokenNotEmpty(TokenOIDC? tokenOIDC) => tokenOIDC?.token.isNotEmpty == true;
|
||||
|
||||
bool _isRefreshTokenNotEmpty() => _token?.refreshToken.isNotEmpty == true;
|
||||
bool _isRefreshTokenNotEmpty(TokenOIDC? tokenOIDC) => tokenOIDC?.refreshToken.isNotEmpty == true;
|
||||
|
||||
bool validateToRefreshToken({int? responseStatusCode}) {
|
||||
if (responseStatusCode == 401 &&
|
||||
_isAuthenticationOidcValid() &&
|
||||
_isTokenNotEmpty() &&
|
||||
_isRefreshTokenNotEmpty() &&
|
||||
_isTokenExpired()
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
bool validateToRefreshToken({
|
||||
required int? responseStatusCode,
|
||||
required TokenOIDC? tokenOIDC
|
||||
}) {
|
||||
return responseStatusCode == 401
|
||||
&& _isAuthenticationOidcValid()
|
||||
&& _isTokenNotEmpty(tokenOIDC)
|
||||
&& _isRefreshTokenNotEmpty(tokenOIDC)
|
||||
&& _isTokenExpired(tokenOIDC);
|
||||
}
|
||||
|
||||
bool validateToRetryTheRequestWithNewToken({
|
||||
required String? authHeader,
|
||||
required TokenOIDC? tokenOIDC
|
||||
}) {
|
||||
return authHeader != null
|
||||
&& _isTokenNotEmpty(tokenOIDC)
|
||||
&& !_isTokenExpired(tokenOIDC)
|
||||
&& !authHeader.contains(tokenOIDC!.token);
|
||||
}
|
||||
|
||||
String _getAuthorizationAsBasicHeader(String? authorization) => 'Basic $authorization';
|
||||
|
||||
@@ -12,6 +12,7 @@ import 'package:tmail_ui_user/features/login/data/local/account_cache_manager.da
|
||||
import 'package:tmail_ui_user/features/login/data/local/token_oidc_cache_manager.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/network/authentication_client/authentication_client_base.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/network/interceptors/authorization_interceptors.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/exceptions/authentication_exception.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
|
||||
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
|
||||
|
||||
@@ -58,7 +59,7 @@ void main() {
|
||||
};
|
||||
final baseOption = BaseOptions(headers: headers);
|
||||
dio = Dio(baseOption)
|
||||
..options.baseUrl = baseUrl;
|
||||
..options.baseUrl = baseUrl;;
|
||||
|
||||
authenticationClient = MockAuthenticationClientBase();
|
||||
tokenOidcCacheManager = MockTokenOidcCacheManager();
|
||||
@@ -85,7 +86,10 @@ void main() {
|
||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||
newConfig: OIDCFixtures.oidcConfiguration);
|
||||
|
||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
||||
final result = authorizationInterceptors.validateToRefreshToken(
|
||||
responseStatusCode: responseStatusCode401,
|
||||
tokenOIDC: OIDCFixtures.tokenOidcExpiredTime,
|
||||
);
|
||||
|
||||
expect(result, true);
|
||||
});
|
||||
@@ -95,7 +99,10 @@ void main() {
|
||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||
newConfig: OIDCFixtures.oidcConfiguration);
|
||||
|
||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode500);
|
||||
final result = authorizationInterceptors.validateToRefreshToken(
|
||||
responseStatusCode: responseStatusCode500,
|
||||
tokenOIDC: OIDCFixtures.tokenOidcExpiredTime,
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
});
|
||||
@@ -105,7 +112,10 @@ void main() {
|
||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||
newConfig: null);
|
||||
|
||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
||||
final result = authorizationInterceptors.validateToRefreshToken(
|
||||
responseStatusCode: responseStatusCode401,
|
||||
tokenOIDC: OIDCFixtures.tokenOidcExpiredTime,
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
});
|
||||
@@ -115,7 +125,10 @@ void main() {
|
||||
newToken: OIDCFixtures.tokenOidcExpiredTimeAndTokenEmpty,
|
||||
newConfig: OIDCFixtures.oidcConfiguration);
|
||||
|
||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
||||
final result = authorizationInterceptors.validateToRefreshToken(
|
||||
responseStatusCode: responseStatusCode401,
|
||||
tokenOIDC: OIDCFixtures.tokenOidcExpiredTimeAndTokenEmpty,
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
});
|
||||
@@ -125,7 +138,10 @@ void main() {
|
||||
newToken: OIDCFixtures.tokenOidcExpiredTimeAndRefreshTokenEmpty,
|
||||
newConfig: OIDCFixtures.oidcConfiguration);
|
||||
|
||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
||||
final result = authorizationInterceptors.validateToRefreshToken(
|
||||
responseStatusCode: responseStatusCode401,
|
||||
tokenOIDC: OIDCFixtures.tokenOidcExpiredTimeAndRefreshTokenEmpty,
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
});
|
||||
@@ -135,7 +151,10 @@ void main() {
|
||||
newToken: OIDCFixtures.newTokenOidc,
|
||||
newConfig: OIDCFixtures.oidcConfiguration);
|
||||
|
||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
||||
final result = authorizationInterceptors.validateToRefreshToken(
|
||||
responseStatusCode: responseStatusCode401,
|
||||
tokenOIDC: OIDCFixtures.newTokenOidc,
|
||||
);
|
||||
|
||||
expect(result, false);
|
||||
});
|
||||
@@ -222,6 +241,135 @@ void main() {
|
||||
});
|
||||
});
|
||||
|
||||
group('AuthorizationInterceptor: multiple requests queued on onError', () {
|
||||
final requestOneDioError401 = DioError(
|
||||
error: {'message': 'Token Expired'},
|
||||
requestOptions: RequestOptions(path: '$baseUrl/1', method: 'POST'),
|
||||
response: Response(
|
||||
statusCode: responseStatusCode401,
|
||||
requestOptions: RequestOptions(path: '$baseUrl/1')
|
||||
),
|
||||
type: DioErrorType.badResponse,
|
||||
);
|
||||
|
||||
final requestTwoDioError401 = DioError(
|
||||
error: {'message': 'Token Expired'},
|
||||
requestOptions: RequestOptions(
|
||||
path: '$baseUrl/2',
|
||||
method: 'POST',
|
||||
headers: {HttpHeaders.authorizationHeader: 'Bearer ${OIDCFixtures.tokenOidcExpiredTime.token}'}
|
||||
),
|
||||
response: Response(
|
||||
statusCode: responseStatusCode401,
|
||||
requestOptions: RequestOptions(path: '$baseUrl/2')
|
||||
),
|
||||
type: DioErrorType.badResponse,
|
||||
);
|
||||
|
||||
test('GIVEN 2 requests have token expired\n'
|
||||
'AND Request 1 refresh token then execute succeeded\n'
|
||||
'THEN Request 2 must use new token to execute request', () async {
|
||||
|
||||
authorizationInterceptors.setTokenAndAuthorityOidc(
|
||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||
newConfig: OIDCFixtures.oidcConfiguration);
|
||||
|
||||
dioAdapter.onPost(
|
||||
'$baseUrl/1',
|
||||
(server) => server.throws(responseStatusCode401, requestOneDioError401)
|
||||
);
|
||||
|
||||
dioAdapter.onPost(
|
||||
'$baseUrl/2',
|
||||
(server) => server.throws(responseStatusCode401, requestTwoDioError401)
|
||||
);
|
||||
|
||||
when(authenticationClient.refreshingTokensOIDC(
|
||||
OIDCFixtures.oidcConfiguration.clientId,
|
||||
OIDCFixtures.oidcConfiguration.redirectUrl,
|
||||
OIDCFixtures.oidcConfiguration.discoveryUrl,
|
||||
OIDCFixtures.oidcConfiguration.scopes,
|
||||
OIDCFixtures.tokenOidcExpiredTime.refreshToken
|
||||
)).thenAnswer((_) async {
|
||||
dioAdapter.onPost(
|
||||
'$baseUrl/1',
|
||||
(server) => server.reply(responseStatusCode200, dataRequestSuccessfully)
|
||||
);
|
||||
dioAdapter.onPost(
|
||||
'$baseUrl/2',
|
||||
(server) => server.reply(responseStatusCode200, dataRequestSuccessfully)
|
||||
);
|
||||
return OIDCFixtures.newTokenOidc;
|
||||
});
|
||||
|
||||
when(accountCacheManager.getCurrentAccount()).thenAnswer((_) async => AccountFixtures.aliceAccount);
|
||||
when(accountCacheManager.deleteCurrentAccount(AccountFixtures.aliceAccount.id)).thenAnswer((_) async {});
|
||||
|
||||
final responses = await Future.wait([
|
||||
dio.post('$baseUrl/1',),
|
||||
dio.post('$baseUrl/2',)
|
||||
]);
|
||||
|
||||
verify(authenticationClient.refreshingTokensOIDC(
|
||||
OIDCFixtures.oidcConfiguration.clientId,
|
||||
OIDCFixtures.oidcConfiguration.redirectUrl,
|
||||
OIDCFixtures.oidcConfiguration.discoveryUrl,
|
||||
OIDCFixtures.oidcConfiguration.scopes,
|
||||
OIDCFixtures.tokenOidcExpiredTime.refreshToken
|
||||
)).called(1);
|
||||
|
||||
expect(responses.length, equals(2));
|
||||
expect(responses[0].statusCode, equals(HttpStatus.ok));
|
||||
expect(responses[0].requestOptions.headers[HttpHeaders.authorizationHeader], equals('Bearer ${OIDCFixtures.newTokenOidc.token}'));
|
||||
|
||||
expect(responses[1].statusCode, equals(HttpStatus.ok));
|
||||
expect(responses[1].requestOptions.headers[HttpHeaders.authorizationHeader], equals('Bearer ${OIDCFixtures.newTokenOidc.token}'));
|
||||
});
|
||||
|
||||
test('GIVEN 2 requests have token expired\n'
|
||||
'AND Request 1 refresh token then execute failed\n'
|
||||
'THEN Request 2 can not execute', () async {
|
||||
|
||||
authorizationInterceptors.setTokenAndAuthorityOidc(
|
||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||
newConfig: OIDCFixtures.oidcConfiguration);
|
||||
|
||||
dioAdapter.onPost(
|
||||
'$baseUrl/1',
|
||||
(server) => server.throws(responseStatusCode401, requestOneDioError401)
|
||||
);
|
||||
|
||||
dioAdapter.onPost(
|
||||
'$baseUrl/2',
|
||||
(server) => server.throws(responseStatusCode401, requestTwoDioError401)
|
||||
);
|
||||
|
||||
when(authenticationClient.refreshingTokensOIDC(
|
||||
OIDCFixtures.oidcConfiguration.clientId,
|
||||
OIDCFixtures.oidcConfiguration.redirectUrl,
|
||||
OIDCFixtures.oidcConfiguration.discoveryUrl,
|
||||
OIDCFixtures.oidcConfiguration.scopes,
|
||||
OIDCFixtures.tokenOidcExpiredTime.refreshToken
|
||||
)).thenAnswer((_) async {
|
||||
throw AccessTokenInvalidException();
|
||||
});
|
||||
|
||||
when(accountCacheManager.getCurrentAccount()).thenAnswer((_) async => AccountFixtures.aliceAccount);
|
||||
when(accountCacheManager.deleteCurrentAccount(AccountFixtures.aliceAccount.id)).thenAnswer((_) async {});
|
||||
|
||||
expect(
|
||||
() async => await Future.wait([
|
||||
dio.post('$baseUrl/1',),
|
||||
dio.post('$baseUrl/2',)
|
||||
]),
|
||||
throwsA(predicate<DioError>(
|
||||
(dioError) => dioError.error is AccessTokenInvalidException))
|
||||
);
|
||||
|
||||
verifyZeroInteractions(authenticationClient);
|
||||
});
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
dioAdapter.close();
|
||||
dio.close();
|
||||
|
||||
Reference in New Issue
Block a user