TF-3833 Fix failed to keep mobile app connect after network issue
Signed-off-by: dab246 <tdvu@linagora.com>
This commit is contained in:
@@ -1,4 +1,3 @@
|
||||
|
||||
import 'package:model/oidc/oidc_configuration.dart';
|
||||
import 'package:model/oidc/response/oidc_discovery_response.dart';
|
||||
import 'package:model/oidc/token_id.dart';
|
||||
|
||||
+88
@@ -0,0 +1,88 @@
|
||||
import 'package:core/utils/app_logger.dart';
|
||||
import 'package:core/utils/platform_info.dart';
|
||||
import 'package:flutter_appauth_platform_interface/flutter_appauth_platform_interface.dart';
|
||||
import 'package:model/oidc/oidc_configuration.dart';
|
||||
import 'package:model/oidc/response/oidc_discovery_response.dart';
|
||||
import 'package:model/oidc/token_id.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/exceptions/oauth_authorization_error.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
|
||||
|
||||
mixin AuthenticationClientInteractionMixin {
|
||||
EndSessionRequest getEndSessionRequest(
|
||||
TokenId tokenId,
|
||||
OIDCConfiguration config,
|
||||
OIDCDiscoveryResponse discoveryResponse,
|
||||
) {
|
||||
final authorizationEndpoint = discoveryResponse.authorizationEndpoint;
|
||||
final tokenEndpoint = discoveryResponse.tokenEndpoint;
|
||||
AuthorizationServiceConfiguration? serviceConfiguration;
|
||||
|
||||
if (authorizationEndpoint != null && tokenEndpoint != null) {
|
||||
serviceConfiguration = AuthorizationServiceConfiguration(
|
||||
authorizationEndpoint: authorizationEndpoint,
|
||||
tokenEndpoint: tokenEndpoint,
|
||||
endSessionEndpoint: discoveryResponse.endSessionEndpoint,
|
||||
);
|
||||
}
|
||||
|
||||
return EndSessionRequest(
|
||||
idTokenHint: tokenId.uuid,
|
||||
postLogoutRedirectUrl: config.logoutRedirectUrl,
|
||||
discoveryUrl: config.discoveryUrl,
|
||||
serviceConfiguration: serviceConfiguration,
|
||||
externalUserAgent: getExternalUserAgent(),
|
||||
);
|
||||
}
|
||||
|
||||
ExternalUserAgent getExternalUserAgent() => PlatformInfo.isIOS
|
||||
? ExternalUserAgent.ephemeralAsWebAuthenticationSession
|
||||
: ExternalUserAgent.asWebAuthenticationSession;
|
||||
|
||||
TokenRequest getRefreshTokenRequest(
|
||||
String clientId,
|
||||
String redirectUrl,
|
||||
String discoveryUrl,
|
||||
String refreshToken,
|
||||
List<String> scopes,
|
||||
) {
|
||||
return TokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
refreshToken: refreshToken,
|
||||
grantType: GrantType.refreshToken,
|
||||
scopes: scopes,
|
||||
);
|
||||
}
|
||||
|
||||
AuthorizationTokenRequest getAuthorizationTokenRequest(
|
||||
String clientId,
|
||||
String redirectUrl,
|
||||
String discoveryUrl,
|
||||
List<String> scopes,
|
||||
) {
|
||||
return AuthorizationTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
scopes: scopes,
|
||||
externalUserAgent: getExternalUserAgent(),
|
||||
);
|
||||
}
|
||||
|
||||
dynamic handleException(dynamic exception) {
|
||||
if (exception is FlutterAppAuthPlatformException) {
|
||||
logError('$runtimeType::handleException: ErrorDetails = ${exception.platformErrorDetails.toString()}');
|
||||
final errorCode = exception.platformErrorDetails.error;
|
||||
if (errorCode != null) {
|
||||
final oauthErrorCode = OAuthAuthorizationError.fromErrorCode(
|
||||
errorCode,
|
||||
errorDescription: exception.platformErrorDetails.errorDescription,
|
||||
);
|
||||
return oauthErrorCode;
|
||||
}
|
||||
}
|
||||
|
||||
return exception;
|
||||
}
|
||||
}
|
||||
+58
-52
@@ -10,82 +10,88 @@ import 'package:model/oidc/token_oidc.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/extensions/token_response_extension.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_interaction_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.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';
|
||||
|
||||
class AuthenticationClientMobile implements AuthenticationClientBase {
|
||||
class AuthenticationClientMobile with AuthenticationClientInteractionMixin
|
||||
implements AuthenticationClientBase {
|
||||
|
||||
final FlutterAppAuth _appAuth;
|
||||
|
||||
AuthenticationClientMobile(this._appAuth);
|
||||
|
||||
@override
|
||||
Future<TokenOIDC> getTokenOIDC(String clientId, String redirectUrl,
|
||||
String discoveryUrl, List<String> scopes) async {
|
||||
Future<TokenOIDC> getTokenOIDC(
|
||||
String clientId,
|
||||
String redirectUrl,
|
||||
String discoveryUrl,
|
||||
List<String> scopes,
|
||||
) async {
|
||||
final authorizationTokenRequest = getAuthorizationTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl,
|
||||
scopes,
|
||||
);
|
||||
final authorizationTokenResponse = await _appAuth.authorizeAndExchangeCode(
|
||||
AuthorizationTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
scopes: scopes,
|
||||
preferEphemeralSession: true));
|
||||
|
||||
log('AuthenticationClientMobile::getTokenOIDC(): token: ${authorizationTokenResponse?.accessToken}');
|
||||
|
||||
if (authorizationTokenResponse != null) {
|
||||
final tokenOIDC = authorizationTokenResponse.toTokenOIDC();
|
||||
if (tokenOIDC.isTokenValid()) {
|
||||
return tokenOIDC;
|
||||
} else {
|
||||
throw AccessTokenInvalidException();
|
||||
}
|
||||
authorizationTokenRequest,
|
||||
);
|
||||
log('$runtimeType::getTokenOIDC(): token: ${authorizationTokenResponse.accessToken}');
|
||||
final tokenOIDC = authorizationTokenResponse.toTokenOIDC();
|
||||
if (tokenOIDC.isTokenValid()) {
|
||||
return tokenOIDC;
|
||||
} else {
|
||||
throw NotFoundAccessTokenException();
|
||||
throw AccessTokenInvalidException();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> logoutOidc(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery) async {
|
||||
final authorizationServiceConfiguration = oidcRescovery.authorizationEndpoint == null || oidcRescovery.tokenEndpoint == null
|
||||
? null
|
||||
: AuthorizationServiceConfiguration(
|
||||
authorizationEndpoint: oidcRescovery.authorizationEndpoint!,
|
||||
tokenEndpoint: oidcRescovery.tokenEndpoint!,
|
||||
endSessionEndpoint: oidcRescovery.endSessionEndpoint);
|
||||
|
||||
final endSession = await _appAuth.endSession(EndSessionRequest(
|
||||
idTokenHint: tokenId.uuid,
|
||||
postLogoutRedirectUrl: config.logoutRedirectUrl,
|
||||
discoveryUrl: config.discoveryUrl,
|
||||
serviceConfiguration: authorizationServiceConfiguration,
|
||||
preferEphemeralSession: true,
|
||||
));
|
||||
log('AuthenticationClientMobile::logoutOidc(): ${endSession?.state}');
|
||||
return endSession?.state?.isNotEmpty == true;
|
||||
Future<bool> logoutOidc(
|
||||
TokenId tokenId,
|
||||
OIDCConfiguration config,
|
||||
OIDCDiscoveryResponse discoveryResponse,
|
||||
) async {
|
||||
final endSessionRequest = getEndSessionRequest(
|
||||
tokenId,
|
||||
config,
|
||||
discoveryResponse,
|
||||
);
|
||||
final endSession = await _appAuth.endSession(endSessionRequest);
|
||||
log('$runtimeType::logoutOidc(): ${endSession.state}');
|
||||
return endSession.state?.isNotEmpty == true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TokenOIDC> refreshingTokensOIDC(String clientId, String redirectUrl,
|
||||
String discoveryUrl, List<String> scopes, String refreshToken) async {
|
||||
final tokenResponse = await _appAuth.token(TokenRequest(
|
||||
Future<TokenOIDC> refreshingTokensOIDC(
|
||||
String clientId,
|
||||
String redirectUrl,
|
||||
String discoveryUrl,
|
||||
List<String> scopes,
|
||||
String refreshToken,
|
||||
) async {
|
||||
try {
|
||||
final tokenRequest = getRefreshTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
refreshToken: refreshToken,
|
||||
scopes: scopes));
|
||||
|
||||
log('AuthenticationClientMobile::refreshingTokensOIDC(): refreshToken: ${tokenResponse?.accessToken}');
|
||||
|
||||
if (tokenResponse != null) {
|
||||
final tokenOIDC = tokenResponse.toTokenOIDC(maybeAvailableRefreshToken: refreshToken);
|
||||
discoveryUrl,
|
||||
refreshToken,
|
||||
scopes,
|
||||
);
|
||||
final tokenResponse = await _appAuth.token(tokenRequest);
|
||||
log('$runtimeType::refreshingTokensOIDC():Token: ${tokenResponse.accessToken}');
|
||||
final tokenOIDC = tokenResponse.toTokenOIDC(
|
||||
maybeAvailableRefreshToken: refreshToken,
|
||||
);
|
||||
if (tokenOIDC.isTokenValid()) {
|
||||
return tokenOIDC;
|
||||
} else {
|
||||
throw AccessTokenInvalidException();
|
||||
}
|
||||
} else {
|
||||
throw NotFoundAccessTokenException();
|
||||
} catch (e) {
|
||||
logError('$runtimeType::refreshingTokensOIDC(): $e');
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,7 +110,7 @@ class AuthenticationClientMobile implements AuthenticationClientBase {
|
||||
intentFlags: ephemeralIntentFlags,
|
||||
),
|
||||
);
|
||||
log('AuthenticationClientMobile::signInTwakeWorkplace():Uri = $uri');
|
||||
log('$runtimeType::signInTwakeWorkplace():Uri = $uri');
|
||||
return TokenOIDC.fromUri(uri);
|
||||
}
|
||||
|
||||
@@ -117,7 +123,7 @@ class AuthenticationClientMobile implements AuthenticationClientBase {
|
||||
intentFlags: ephemeralIntentFlags,
|
||||
),
|
||||
);
|
||||
log('AuthenticationClientMobile::signUpTwakeWorkplace():Uri = $uri');
|
||||
log('$runtimeType::signUpTwakeWorkplace():Uri = $uri');
|
||||
return TokenOIDC.fromUri(uri);
|
||||
}
|
||||
}
|
||||
|
||||
+85
-70
@@ -9,89 +9,104 @@ import 'package:model/oidc/token_oidc.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/extensions/token_response_extension.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_interaction_mixin.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/utils/library_platform/app_auth_plugin/app_auth_plugin.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';
|
||||
|
||||
class AuthenticationClientWeb implements AuthenticationClientBase {
|
||||
class AuthenticationClientWeb with AuthenticationClientInteractionMixin
|
||||
implements AuthenticationClientBase {
|
||||
|
||||
final AppAuthWebPlugin _appAuthWeb;
|
||||
|
||||
AuthenticationClientWeb(this._appAuthWeb);
|
||||
|
||||
@override
|
||||
Future<TokenOIDC> getTokenOIDC(String clientId, String redirectUrl,
|
||||
String discoveryUrl, List<String> scopes) async {
|
||||
final authorizationTokenResponse = await _appAuthWeb.authorizeAndExchangeCode(AuthorizationTokenRequest(
|
||||
Future<TokenOIDC> getTokenOIDC(
|
||||
String clientId,
|
||||
String redirectUrl,
|
||||
String discoveryUrl,
|
||||
List<String> scopes,
|
||||
) async {
|
||||
final authorizationTokenRequest = getAuthorizationTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl,
|
||||
scopes,
|
||||
);
|
||||
final authorizationTokenResponse = await _appAuthWeb.authorizeAndExchangeCode(
|
||||
authorizationTokenRequest,
|
||||
);
|
||||
log('$runtimeType::getTokenOIDC():Token: ${authorizationTokenResponse.accessToken}');
|
||||
final tokenOIDC = authorizationTokenResponse.toTokenOIDC();
|
||||
if (tokenOIDC.isTokenValid()) {
|
||||
return tokenOIDC;
|
||||
} else {
|
||||
throw AccessTokenInvalidException();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> logoutOidc(
|
||||
TokenId tokenId,
|
||||
OIDCConfiguration config,
|
||||
OIDCDiscoveryResponse discoveryResponse,
|
||||
) async {
|
||||
final endSessionRequest = getEndSessionRequest(
|
||||
tokenId,
|
||||
config,
|
||||
discoveryResponse,
|
||||
);
|
||||
final endSession = await _appAuthWeb.endSession(endSessionRequest);
|
||||
log('$runtimeType::logoutOidc(): ${endSession.state}');
|
||||
return endSession.state?.isNotEmpty == true;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TokenOIDC> refreshingTokensOIDC(
|
||||
String clientId,
|
||||
String redirectUrl,
|
||||
String discoveryUrl,
|
||||
List<String> scopes,
|
||||
String refreshToken,
|
||||
) async {
|
||||
try {
|
||||
final tokenRequest = getRefreshTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl,
|
||||
refreshToken,
|
||||
scopes,
|
||||
);
|
||||
final tokenResponse = await _appAuthWeb.token(tokenRequest);
|
||||
final tokenOIDC = tokenResponse.toTokenOIDC(
|
||||
maybeAvailableRefreshToken: refreshToken,
|
||||
);
|
||||
if (tokenOIDC.isTokenValid()) {
|
||||
return tokenOIDC;
|
||||
} else {
|
||||
throw AccessTokenInvalidException();
|
||||
}
|
||||
} catch (e) {
|
||||
logError('$runtimeType::refreshingTokensOIDC(): $e');
|
||||
throw handleException(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> authenticateOidcOnBrowser(
|
||||
String clientId,
|
||||
String redirectUrl,
|
||||
String discoveryUrl,
|
||||
List<String> scopes,
|
||||
) async {
|
||||
await _appAuthWeb.authorizeAndExchangeCode(
|
||||
AuthorizationTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
scopes: scopes,
|
||||
preferEphemeralSession: true));
|
||||
|
||||
log('AuthClientMobile::getTokenOIDC(): token: ${authorizationTokenResponse?.accessToken}');
|
||||
|
||||
if (authorizationTokenResponse != null) {
|
||||
final tokenOIDC = authorizationTokenResponse.toTokenOIDC();
|
||||
if (tokenOIDC.isTokenValid()) {
|
||||
return tokenOIDC;
|
||||
} else {
|
||||
throw AccessTokenInvalidException();
|
||||
}
|
||||
} else {
|
||||
throw NotFoundAccessTokenException();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> logoutOidc(TokenId tokenId, OIDCConfiguration config, OIDCDiscoveryResponse oidcRescovery) async {
|
||||
final authorizationServiceConfiguration = oidcRescovery.authorizationEndpoint == null || oidcRescovery.tokenEndpoint == null
|
||||
? null
|
||||
: AuthorizationServiceConfiguration(
|
||||
authorizationEndpoint: oidcRescovery.authorizationEndpoint!,
|
||||
tokenEndpoint: oidcRescovery.tokenEndpoint!,
|
||||
endSessionEndpoint: oidcRescovery.endSessionEndpoint);
|
||||
final endSession = await _appAuthWeb.endSession(EndSessionRequest(
|
||||
idTokenHint: tokenId.uuid,
|
||||
postLogoutRedirectUrl: config.logoutRedirectUrl,
|
||||
discoveryUrl: config.discoveryUrl,
|
||||
serviceConfiguration: authorizationServiceConfiguration
|
||||
));
|
||||
return endSession != null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<TokenOIDC> refreshingTokensOIDC(String clientId, String redirectUrl,
|
||||
String discoveryUrl, List<String> scopes, String refreshToken) async {
|
||||
final tokenResponse = await _appAuthWeb.token(TokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
refreshToken: refreshToken,
|
||||
grantType: 'refresh_token',
|
||||
scopes: scopes));
|
||||
|
||||
if (tokenResponse != null) {
|
||||
final tokenOIDC = tokenResponse.toTokenOIDC(maybeAvailableRefreshToken: refreshToken);
|
||||
if (tokenOIDC.isTokenValid()) {
|
||||
return tokenOIDC;
|
||||
} else {
|
||||
throw AccessTokenInvalidException();
|
||||
}
|
||||
} else {
|
||||
throw NotFoundAccessTokenException();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> authenticateOidcOnBrowser(String clientId, String redirectUrl,
|
||||
String discoveryUrl, List<String> scopes) async {
|
||||
await _appAuthWeb.authorizeAndExchangeCode(
|
||||
AuthorizationTokenRequest(
|
||||
clientId,
|
||||
redirectUrl,
|
||||
discoveryUrl: discoveryUrl,
|
||||
scopes: scopes));
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -14,6 +14,7 @@ import 'package:model/oidc/token_oidc.dart';
|
||||
import 'package:tmail_ui_user/features/login/data/local/account_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/domain/exceptions/oauth_authorization_error.dart';
|
||||
import 'package:tmail_ui_user/features/login/domain/extensions/oidc_configuration_extensions.dart';
|
||||
import 'package:tmail_ui_user/features/upload/data/network/file_uploader.dart';
|
||||
import 'package:tmail_ui_user/main/utils/ios_sharing_manager.dart';
|
||||
@@ -152,7 +153,14 @@ class AuthorizationInterceptors extends QueuedInterceptorsWrapper {
|
||||
}
|
||||
} catch (e) {
|
||||
logError('AuthorizationInterceptors::onError:Exception: $e');
|
||||
return super.onError(err.copyWith(error: e), handler);
|
||||
if (e is ServerError || e is TemporarilyUnavailable) {
|
||||
return super.onError(
|
||||
DioError(requestOptions: err.requestOptions, error: e),
|
||||
handler,
|
||||
);
|
||||
} else {
|
||||
return super.onError(err.copyWith(error: e), handler);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,8 +25,6 @@ class InvalidBaseUrl extends AuthenticationException {
|
||||
InvalidBaseUrl() : super(AuthenticationException.invalidBaseUrl);
|
||||
}
|
||||
|
||||
class NotFoundAccessTokenException implements Exception {}
|
||||
|
||||
class AccessTokenInvalidException implements Exception {}
|
||||
|
||||
class DownloadAttachmentHasTokenExpiredException implements Exception {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
class OAuthAuthorizationError {
|
||||
static const String serverError = 'server_error'; // HTTP 500 error code
|
||||
static const String temporarilyUnavailable =
|
||||
'temporarily_unavailable'; // HTTP 503 error code
|
||||
|
||||
final String error;
|
||||
final String? errorDescription;
|
||||
|
||||
const OAuthAuthorizationError({
|
||||
required this.error,
|
||||
this.errorDescription,
|
||||
});
|
||||
|
||||
static OAuthAuthorizationError fromErrorCode(
|
||||
String error, {
|
||||
String? errorDescription,
|
||||
}) {
|
||||
switch (error) {
|
||||
case serverError:
|
||||
return ServerError(errorDescription: errorDescription);
|
||||
case temporarilyUnavailable:
|
||||
return TemporarilyUnavailable(errorDescription: errorDescription);
|
||||
default:
|
||||
return OAuthAuthorizationError(
|
||||
error: error,
|
||||
errorDescription: errorDescription,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ServerError extends OAuthAuthorizationError {
|
||||
const ServerError({super.errorDescription})
|
||||
: super(error: OAuthAuthorizationError.serverError);
|
||||
}
|
||||
|
||||
class TemporarilyUnavailable extends OAuthAuthorizationError {
|
||||
const TemporarilyUnavailable({super.errorDescription})
|
||||
: super(error: OAuthAuthorizationError.temporarilyUnavailable);
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import 'package:core/presentation/state/failure.dart';
|
||||
import 'package:core/presentation/state/success.dart';
|
||||
import 'package:model/oidc/token_oidc.dart';
|
||||
|
||||
class RefreshTokenOIDCSuccess extends UIState {
|
||||
|
||||
final TokenOIDC tokenOIDC;
|
||||
|
||||
RefreshTokenOIDCSuccess(this.tokenOIDC);
|
||||
|
||||
@override
|
||||
List<Object> get props => [tokenOIDC];
|
||||
}
|
||||
|
||||
class RefreshTokenOIDCFailure extends FeatureFailure {
|
||||
|
||||
RefreshTokenOIDCFailure(dynamic exception) : super(exception: exception);
|
||||
}
|
||||
Reference in New Issue
Block a user