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;
|
_token = newToken;
|
||||||
_configOIDC = newConfig;
|
_configOIDC = newConfig;
|
||||||
_authenticationType = AuthenticationType.oidc;
|
_authenticationType = AuthenticationType.oidc;
|
||||||
log('AuthorizationInterceptors::setTokenAndAuthorityOidc: TOKEN_INITIAL = $newToken');
|
log('AuthorizationInterceptors::setTokenAndAuthorityOidc: INITIAL_TOKEN = ${newToken?.token} | EXPIRED_TIME = ${newToken?.expiredTime}');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _updateNewToken(TokenOIDC newToken) {
|
void _updateNewToken(TokenOIDC newToken) {
|
||||||
log('AuthorizationInterceptors::_updateNewToken: NEW_TOKEN = $newToken');
|
log('AuthorizationInterceptors::_updateNewToken: NEW_TOKEN = ${newToken.token} | EXPIRED_TIME = ${newToken.expiredTime}');
|
||||||
_token = newToken;
|
_token = newToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,13 +83,17 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void onError(DioError err, ErrorInterceptorHandler handler) async {
|
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 {
|
try {
|
||||||
if (validateToRefreshToken(responseStatusCode: err.response?.statusCode)) {
|
final requestOptions = err.requestOptions;
|
||||||
log('AuthorizationInterceptors::onError:_validateToRefreshToken');
|
final extraInRequest = requestOptions.extra;
|
||||||
final requestOptions = err.requestOptions;
|
bool isRetryRequest = false;
|
||||||
final extraInRequest = requestOptions.extra;
|
|
||||||
|
|
||||||
|
if (validateToRefreshToken(
|
||||||
|
responseStatusCode: err.response?.statusCode,
|
||||||
|
tokenOIDC: _token
|
||||||
|
)) {
|
||||||
|
log('AuthorizationInterceptors::onError:_validateToRefreshToken');
|
||||||
final newTokenOidc = PlatformInfo.isIOS
|
final newTokenOidc = PlatformInfo.isIOS
|
||||||
? await _handleRefreshTokenOnIOSPlatform()
|
? await _handleRefreshTokenOnIOSPlatform()
|
||||||
: await _handleRefreshTokenOnOtherPlatform();
|
: await _handleRefreshTokenOnOtherPlatform();
|
||||||
@@ -101,6 +105,18 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
|
|||||||
|
|
||||||
_updateNewToken(newTokenOidc);
|
_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)) {
|
if (extraInRequest.containsKey(FileUploader.uploadAttachmentExtraKey)) {
|
||||||
log('AuthorizationInterceptors::onError: Perform upload attachment request');
|
log('AuthorizationInterceptors::onError: Perform upload attachment request');
|
||||||
final uploadExtra = extraInRequest[FileUploader.uploadAttachmentExtraKey];
|
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 _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}) {
|
bool validateToRefreshToken({
|
||||||
if (responseStatusCode == 401 &&
|
required int? responseStatusCode,
|
||||||
_isAuthenticationOidcValid() &&
|
required TokenOIDC? tokenOIDC
|
||||||
_isTokenNotEmpty() &&
|
}) {
|
||||||
_isRefreshTokenNotEmpty() &&
|
return responseStatusCode == 401
|
||||||
_isTokenExpired()
|
&& _isAuthenticationOidcValid()
|
||||||
) {
|
&& _isTokenNotEmpty(tokenOIDC)
|
||||||
return true;
|
&& _isRefreshTokenNotEmpty(tokenOIDC)
|
||||||
}
|
&& _isTokenExpired(tokenOIDC);
|
||||||
return false;
|
}
|
||||||
|
|
||||||
|
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';
|
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/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/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/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/features/login/domain/extensions/oidc_configuration_extensions.dart';
|
||||||
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
|
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ void main() {
|
|||||||
};
|
};
|
||||||
final baseOption = BaseOptions(headers: headers);
|
final baseOption = BaseOptions(headers: headers);
|
||||||
dio = Dio(baseOption)
|
dio = Dio(baseOption)
|
||||||
..options.baseUrl = baseUrl;
|
..options.baseUrl = baseUrl;;
|
||||||
|
|
||||||
authenticationClient = MockAuthenticationClientBase();
|
authenticationClient = MockAuthenticationClientBase();
|
||||||
tokenOidcCacheManager = MockTokenOidcCacheManager();
|
tokenOidcCacheManager = MockTokenOidcCacheManager();
|
||||||
@@ -85,7 +86,10 @@ void main() {
|
|||||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||||
newConfig: OIDCFixtures.oidcConfiguration);
|
newConfig: OIDCFixtures.oidcConfiguration);
|
||||||
|
|
||||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
final result = authorizationInterceptors.validateToRefreshToken(
|
||||||
|
responseStatusCode: responseStatusCode401,
|
||||||
|
tokenOIDC: OIDCFixtures.tokenOidcExpiredTime,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, true);
|
expect(result, true);
|
||||||
});
|
});
|
||||||
@@ -95,7 +99,10 @@ void main() {
|
|||||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||||
newConfig: OIDCFixtures.oidcConfiguration);
|
newConfig: OIDCFixtures.oidcConfiguration);
|
||||||
|
|
||||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode500);
|
final result = authorizationInterceptors.validateToRefreshToken(
|
||||||
|
responseStatusCode: responseStatusCode500,
|
||||||
|
tokenOIDC: OIDCFixtures.tokenOidcExpiredTime,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, false);
|
expect(result, false);
|
||||||
});
|
});
|
||||||
@@ -105,7 +112,10 @@ void main() {
|
|||||||
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
newToken: OIDCFixtures.tokenOidcExpiredTime,
|
||||||
newConfig: null);
|
newConfig: null);
|
||||||
|
|
||||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
final result = authorizationInterceptors.validateToRefreshToken(
|
||||||
|
responseStatusCode: responseStatusCode401,
|
||||||
|
tokenOIDC: OIDCFixtures.tokenOidcExpiredTime,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, false);
|
expect(result, false);
|
||||||
});
|
});
|
||||||
@@ -115,7 +125,10 @@ void main() {
|
|||||||
newToken: OIDCFixtures.tokenOidcExpiredTimeAndTokenEmpty,
|
newToken: OIDCFixtures.tokenOidcExpiredTimeAndTokenEmpty,
|
||||||
newConfig: OIDCFixtures.oidcConfiguration);
|
newConfig: OIDCFixtures.oidcConfiguration);
|
||||||
|
|
||||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
final result = authorizationInterceptors.validateToRefreshToken(
|
||||||
|
responseStatusCode: responseStatusCode401,
|
||||||
|
tokenOIDC: OIDCFixtures.tokenOidcExpiredTimeAndTokenEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, false);
|
expect(result, false);
|
||||||
});
|
});
|
||||||
@@ -125,7 +138,10 @@ void main() {
|
|||||||
newToken: OIDCFixtures.tokenOidcExpiredTimeAndRefreshTokenEmpty,
|
newToken: OIDCFixtures.tokenOidcExpiredTimeAndRefreshTokenEmpty,
|
||||||
newConfig: OIDCFixtures.oidcConfiguration);
|
newConfig: OIDCFixtures.oidcConfiguration);
|
||||||
|
|
||||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
final result = authorizationInterceptors.validateToRefreshToken(
|
||||||
|
responseStatusCode: responseStatusCode401,
|
||||||
|
tokenOIDC: OIDCFixtures.tokenOidcExpiredTimeAndRefreshTokenEmpty,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, false);
|
expect(result, false);
|
||||||
});
|
});
|
||||||
@@ -135,7 +151,10 @@ void main() {
|
|||||||
newToken: OIDCFixtures.newTokenOidc,
|
newToken: OIDCFixtures.newTokenOidc,
|
||||||
newConfig: OIDCFixtures.oidcConfiguration);
|
newConfig: OIDCFixtures.oidcConfiguration);
|
||||||
|
|
||||||
final result = authorizationInterceptors.validateToRefreshToken(responseStatusCode: responseStatusCode401);
|
final result = authorizationInterceptors.validateToRefreshToken(
|
||||||
|
responseStatusCode: responseStatusCode401,
|
||||||
|
tokenOIDC: OIDCFixtures.newTokenOidc,
|
||||||
|
);
|
||||||
|
|
||||||
expect(result, false);
|
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(() {
|
tearDown(() {
|
||||||
dioAdapter.close();
|
dioAdapter.close();
|
||||||
dio.close();
|
dio.close();
|
||||||
|
|||||||
Reference in New Issue
Block a user