Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[HOT-FIX] Fix refresh token with oidc on upn #2133

Merged
merged 2 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions docs/adr/0031-fix-refresh-token-with-oidc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# 30. Fix refresh token with OIDC using `QueuedInterceptor`

Date: 2023-09-11

## Status

- Issue:

[1974](https://github.com/linagora/tmail-flutter/issues/1974)

## Context

- Requests still return `401` after retrieving a new token. The application automatically logs out

## Root cause

- When executing tasks concurrently, they are pushed into the `Queue` along with the old `header` values (the old authentication is retained).
So when the first request receives a `401` error and tries to get a new token, it will be updated with the new authentication header value.

## Decision

- Use `QueuedInterceptor` to serialize `requests/responses/errors` before they enter the interceptor.
If there are multiple concurrent requests, the request is added to a queue before entering the interceptor.
Only one request at a time enters the interceptor, and after that request is processed by the interceptor, the next request will enter the interceptor.
- Try to make a `retry` request up to 3 times when receiving a `401` error. Aims to update the new token value on `memmory` to requests in the `queue`.

## Consequences

- The following `requests` were completed successfully. The application is not automatically logged out
136 changes: 75 additions & 61 deletions lib/features/login/data/network/config/authorization_interceptors.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ import 'package:tmail_ui_user/features/login/data/network/authentication_client/

class AuthorizationInterceptors extends QueuedInterceptorsWrapper {

static const int _maxRetryCount = 3;
static const String RETRY_KEY = 'Retry';

final Dio _dio;
final AuthenticationClientBase _authenticationClient;
final TokenOidcCacheManager _tokenOidcCacheManager;
Expand All @@ -41,7 +44,6 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
_token = newToken;
_configOIDC = newConfig;
_authenticationType = AuthenticationType.oidc;
log('AuthorizationInterceptors::setToken(): newToken: $newToken | configOIDC: $_configOIDC');
}

void _updateNewToken(Token newToken) {
Expand All @@ -54,6 +56,8 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {

@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
log('AuthorizationInterceptors::onRequest():DATA: ${options.data}');
log('AuthorizationInterceptors::onRequest():TOKEN_HASHCODE_CURRENT: ${_token?.token.hashCode}');
switch(_authenticationType) {
case AuthenticationType.basic:
if (_authorization != null) {
Expand All @@ -73,77 +77,87 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {

@override
void onError(DioError err, ErrorInterceptorHandler handler) async {
logError('AuthorizationInterceptors::onError():dioType: ${err.type} | statusCode: ${err.response?.statusCode} | message: ${err.message} | statusMessage: ${err.response?.statusMessage}');
try {
if (_validateToRefreshToken(err)) {
log('AuthorizationInterceptors::onError:RefreshTokenCalled:configOIDC: $_configOIDC | refreshTokenCurrent: ${_token?.refreshToken}');
final newToken = await _authenticationClient.refreshingTokensOIDC(
_configOIDC!.clientId,
_configOIDC!.redirectUrl,
_configOIDC!.discoveryUrl,
_configOIDC!.scopes,
_token!.refreshToken
);

final accountCurrent = await _accountCacheManager.getSelectedAccount();

await _accountCacheManager.deleteSelectedAccount(_token!.tokenIdHash);

await Future.wait([
_tokenOidcCacheManager.persistOneTokenOidc(newToken),
_accountCacheManager.setSelectedAccount(
PersonalAccount(
newToken.tokenIdHash,
AuthenticationType.oidc,
isSelected: true,
accountId: accountCurrent.accountId,
apiUrl: accountCurrent.apiUrl,
userName: accountCurrent.userName
)
logError('AuthorizationInterceptors::onError(): $err');
logError('AuthorizationInterceptors::onError():TOKEN_HASHCODE_CURRENT: ${_token?.token.hashCode}');

final requestOptions = err.requestOptions;
final extraInRequest = requestOptions.extra;
var retries = extraInRequest[RETRY_KEY] ?? 0;

if (_validateToRefreshToken(err)) {
log('AuthorizationInterceptors::onError:>> _validateToRefreshToken');
final newToken = await _authenticationClient.refreshingTokensOIDC(
_configOIDC!.clientId,
_configOIDC!.redirectUrl,
_configOIDC!.discoveryUrl,
_configOIDC!.scopes,
_token!.refreshToken
);

final accountCurrent = await _accountCacheManager.getSelectedAccount();

await _accountCacheManager.deleteSelectedAccount(_token!.tokenIdHash);

await Future.wait([
_tokenOidcCacheManager.persistOneTokenOidc(newToken),
_accountCacheManager.setSelectedAccount(
PersonalAccount(
newToken.tokenIdHash,
AuthenticationType.oidc,
isSelected: true,
accountId: accountCurrent.accountId,
apiUrl: accountCurrent.apiUrl,
userName: accountCurrent.userName
)
]);
log('AuthorizationInterceptors::onError():NewToken: $newToken');
_updateNewToken(newToken.toToken());

final requestOptions = err.requestOptions;
requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(newToken.token);

final response = await _dio.fetch(requestOptions);
return handler.resolve(response);
} else {
super.onError(err, handler);
}
} catch (e) {
logError('AuthorizationInterceptors::onError():Exception: $e');
)
]);
_updateNewToken(newToken.toToken());

final requestOptions = err.requestOptions;
requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(newToken.token);

final response = await _dio.fetch(requestOptions);
return handler.resolve(response);
} else if (_validateToRetry(err, retries)) {
log('AuthorizationInterceptors::onError:>> _validateToRetry | retries: $retries');
retries++;

final requestOptions = err.requestOptions;
requestOptions.headers[HttpHeaders.authorizationHeader] = _getTokenAsBearerHeader(_token!.token);
requestOptions.extra = {RETRY_KEY: retries};

final response = await _dio.fetch(requestOptions);
return handler.resolve(response);
} else {
super.onError(err, handler);
}
}

bool _isTokenExpired() {
if (_token?.isExpired == true) {
log('AuthorizationInterceptors::_isTokenExpired(): TOKE_EXPIRED');
return true;
}
return false;
}
bool _isTokenExpired() => _token?.isExpired == true;

bool _isAuthenticationOidcValid() => _authenticationType == AuthenticationType.oidc && _configOIDC != null;

bool _isTokenNotEmpty() => _token?.token.isNotEmpty == true;

bool _isAuthenticationOidcValid() {
if (_authenticationType == AuthenticationType.oidc &&
_configOIDC != null &&
_token != null) {
log('AuthorizationInterceptors::_isAuthenticationOidcValid()');
bool _isRefreshTokenNotEmpty() => _token?.refreshToken.isNotEmpty == true;

bool _validateToRefreshToken(DioError dioError) {
if (dioError.response?.statusCode == 401 &&
_isAuthenticationOidcValid() &&
_isRefreshTokenNotEmpty() &&
_isTokenExpired()
) {
return true;
}
return false;
}

bool _isRefreshTokenNotEmpty() => _token != null && _token!.refreshToken.isNotEmpty;

bool _validateToRefreshToken(DioError dioError) {
if (_isTokenExpired() &&
(dioError.response == null || dioError.response?.statusCode == 401) &&
_isRefreshTokenNotEmpty() &&
_isAuthenticationOidcValid()) {
bool _validateToRetry(DioError dioError, int retryCount) {
if (dioError.type == DioErrorType.badResponse &&
dioError.response?.statusCode == 401 &&
_isTokenNotEmpty() &&
retryCount < _maxRetryCount
) {
return true;
}
return false;
Expand Down
Loading