From 4804a1659a8c3fe4a6a54e777979bb53fdba2582 Mon Sep 17 00:00:00 2001 From: dab246 Date: Fri, 20 May 2022 15:17:57 +0700 Subject: [PATCH] TF-571 Get token from authentication sso --- ios/Runner/Info.plist | 2 +- .../authentication_oidc_datasource.dart | 2 ++ .../authentication_oidc_datasource_impl.dart | 9 +++++ .../authentication_token_extension.dart | 14 ++++++++ .../login/data/network/oidc_http_client.dart | 23 +++++++++++- .../authentication_oidc_repository_impl.dart | 6 ++++ .../authentication_oidc_repository.dart | 5 +-- .../domain/state/get_token_oidc_state.dart | 21 +++++++++++ .../usecases/get_token_oidc_interactor.dart | 22 ++++++++++++ .../login/presentation/base_login_view.dart | 3 ++ .../login/presentation/login_bindings.dart | 11 +++--- .../login/presentation/login_controller.dart | 33 +++++++++++++++++ .../login/presentation/state/login_state.dart | 6 ++++ .../bindings/network/network_bindings.dart | 4 +++ lib/main/localizations/app_localizations.dart | 6 ++++ model/lib/model.dart | 5 ++- model/lib/oidc/token.dart | 17 +++++++++ model/lib/oidc/token_id.dart | 11 ++++++ model/lib/oidc/token_oidc.dart | 35 +++++++++++++++++++ pubspec.yaml | 3 ++ 20 files changed, 227 insertions(+), 11 deletions(-) create mode 100644 lib/features/login/data/extensions/authentication_token_extension.dart create mode 100644 lib/features/login/domain/state/get_token_oidc_state.dart create mode 100644 lib/features/login/domain/usecases/get_token_oidc_interactor.dart create mode 100644 model/lib/oidc/token.dart create mode 100644 model/lib/oidc/token_id.dart create mode 100644 model/lib/oidc/token_oidc.dart diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index a2d9954c8..f7bbc06a8 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -31,7 +31,7 @@ CFBundleURLSchemes ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) - teammail-mobile + teammail.mobile diff --git a/lib/features/login/data/datasource/authentication_oidc_datasource.dart b/lib/features/login/data/datasource/authentication_oidc_datasource.dart index 1f474497b..53e91f8bc 100644 --- a/lib/features/login/data/datasource/authentication_oidc_datasource.dart +++ b/lib/features/login/data/datasource/authentication_oidc_datasource.dart @@ -5,4 +5,6 @@ abstract class AuthenticationOIDCDataSource { Future checkOIDCIsAvailable(OIDCRequest oidcRequest); Future getOIDCConfiguration(Uri baseUri); + + Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes); } \ No newline at end of file diff --git a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart index 05e4dba31..3a22cca86 100644 --- a/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart +++ b/lib/features/login/data/datasource_impl/authentication_oidc_datasource_impl.dart @@ -26,4 +26,13 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { throw error; }); } + + @override + Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes) { + return Future.sync(() async { + return await _oidcHttpClient.getTokenOIDC(clientId, redirectUrl, discoveryUrl, scopes); + }).catchError((error) { + throw error; + }); + } } \ No newline at end of file diff --git a/lib/features/login/data/extensions/authentication_token_extension.dart b/lib/features/login/data/extensions/authentication_token_extension.dart new file mode 100644 index 000000000..fed776cd7 --- /dev/null +++ b/lib/features/login/data/extensions/authentication_token_extension.dart @@ -0,0 +1,14 @@ + +import 'package:flutter_appauth/flutter_appauth.dart'; +import 'package:model/model.dart'; + +extension AuthorizationTokenResponseExtension on AuthorizationTokenResponse { + + TokenOIDC toTokenOIDC() { + return TokenOIDC( + accessToken ?? '', + TokenId(idToken ?? ''), + refreshToken ?? '', + expiredTime: accessTokenExpirationDateTime ?? DateTime.now()); + } +} \ No newline at end of file diff --git a/lib/features/login/data/network/oidc_http_client.dart b/lib/features/login/data/network/oidc_http_client.dart index 142b6feaa..ad335827b 100644 --- a/lib/features/login/data/network/oidc_http_client.dart +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -2,15 +2,18 @@ import 'dart:convert'; import 'package:core/core.dart'; +import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:model/model.dart'; +import 'package:tmail_ui_user/features/login/data/extensions/authentication_token_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; class OIDCHttpClient { final DioClient _dioClient; + final FlutterAppAuth _appAuth; - OIDCHttpClient(this._dioClient); + OIDCHttpClient(this._dioClient, this._appAuth); Future checkOIDCIsAvailable(OIDCRequest oidcRequest) async { final result = await _dioClient.get( @@ -33,4 +36,22 @@ class OIDCHttpClient { Future getOIDCConfiguration(Uri baseUri) async { return OIDCConfiguration.initial(); } + + Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes) async { + final tokenResponse = await _appAuth.authorizeAndExchangeCode(AuthorizationTokenRequest( + clientId, + redirectUrl, + discoveryUrl: discoveryUrl, + scopes: scopes, + preferEphemeralSession: true)); + + log('OIDCHttpClient::getTokenOIDC(): token: ${tokenResponse?.accessToken}'); + + if (tokenResponse != null) { + final tokenOIDC = tokenResponse.toTokenOIDC(); + return tokenOIDC.isTokenValid() ? tokenOIDC : TokenOIDC.empty(); + } else { + return TokenOIDC.empty(); + } + } } \ No newline at end of file diff --git a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart index 2de8dcb19..e4de26706 100644 --- a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart @@ -1,5 +1,6 @@ import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; +import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; @@ -17,4 +18,9 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { Future getOIDCConfiguration(Uri baseUri) { return _oidcDataSource.getOIDCConfiguration(baseUri); } + + @override + Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes) { + return _oidcDataSource.getTokenOIDC(clientId, redirectUrl, discoveryUrl, scopes); + } } \ No newline at end of file diff --git a/lib/features/login/domain/repository/authentication_oidc_repository.dart b/lib/features/login/domain/repository/authentication_oidc_repository.dart index b52915522..699076b6a 100644 --- a/lib/features/login/domain/repository/authentication_oidc_repository.dart +++ b/lib/features/login/domain/repository/authentication_oidc_repository.dart @@ -1,9 +1,10 @@ -import 'package:model/oidc/oidc_configuration.dart'; -import 'package:model/oidc/request/oidc_request.dart'; +import 'package:model/model.dart'; abstract class AuthenticationOIDCRepository { Future checkOIDCIsAvailable(OIDCRequest oidcRequest); Future getOIDCConfiguration(Uri baseUri); + + Future getTokenOIDC(String clientId, String redirectUrl, String discoveryUrl, List scopes); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_token_oidc_state.dart b/lib/features/login/domain/state/get_token_oidc_state.dart new file mode 100644 index 000000000..6b0e8b5a1 --- /dev/null +++ b/lib/features/login/domain/state/get_token_oidc_state.dart @@ -0,0 +1,21 @@ +import 'package:core/core.dart'; +import 'package:model/model.dart'; + +class GetTokenOIDCSuccess extends UIState { + + final TokenOIDC tokenOIDC; + + GetTokenOIDCSuccess(this.tokenOIDC); + + @override + List get props => [tokenOIDC]; +} + +class GetTokenOIDCFailure extends FeatureFailure { + final dynamic exception; + + GetTokenOIDCFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/get_token_oidc_interactor.dart b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart new file mode 100644 index 000000000..0e956cf2c --- /dev/null +++ b/lib/features/login/domain/usecases/get_token_oidc_interactor.dart @@ -0,0 +1,22 @@ + +import 'package:core/core.dart'; +import 'package:dartz/dartz.dart'; +import 'package:tmail_ui_user/features/login/domain/repository/authentication_oidc_repository.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.dart'; + +class GetTokenOIDCInteractor { + + final AuthenticationOIDCRepository authenticationOIDCRepository; + + GetTokenOIDCInteractor(this.authenticationOIDCRepository); + + Future> execute(String clientId, String redirectUrl, + String discoveryUrl, List scopes) async { + try { + final tokenOIDC = await authenticationOIDCRepository.getTokenOIDC(clientId, redirectUrl, discoveryUrl, scopes); + return Right(GetTokenOIDCSuccess(tokenOIDC)); + } catch (e) { + return Left(GetTokenOIDCFailure(e)); + } + } +} \ No newline at end of file diff --git a/lib/features/login/presentation/base_login_view.dart b/lib/features/login/presentation/base_login_view.dart index bed91c858..b147aa87e 100644 --- a/lib/features/login/presentation/base_login_view.dart +++ b/lib/features/login/presentation/base_login_view.dart @@ -7,6 +7,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; @@ -39,6 +40,8 @@ abstract class BaseLoginView extends GetWidget { } else if (failure is GetOIDCConfigurationFailure || failure is LoginCanNotVerifySSOConfigurationAction) { return AppLocalizations.of(context).canNotVerifySSOConfiguration; + } else if (failure is GetTokenOIDCFailure || failure is LoginCanNotGetTokenAction) { + return AppLocalizations.of(context).canNotGetToken; } else { return AppLocalizations.of(context).unknown_error_login_message; } diff --git a/lib/features/login/presentation/login_bindings.dart b/lib/features/login/presentation/login_bindings.dart index bd1bf22d7..88d5e1db4 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -16,16 +16,11 @@ import 'package:tmail_ui_user/features/login/domain/repository/credential_reposi import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_configuration_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_controller.dart'; class LoginBindings extends BaseBindings { - @override - void dependencies() { - Get.lazyPut(() => OIDCHttpClient(Get.find())); - super.dependencies(); - } - @override void bindingsController() { Get.lazyPut(() => LoginController( @@ -34,6 +29,7 @@ class LoginBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @@ -61,6 +57,9 @@ class LoginBindings extends BaseBindings { Get.lazyPut(() => GetOIDCConfigurationInteractor( Get.find(), )); + Get.lazyPut(() => GetTokenOIDCInteractor( + Get.find(), + )); } @override diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index f9a42fc57..cded59c94 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -7,9 +7,11 @@ import 'package:model/model.dart'; import 'package:tmail_ui_user/features/login/domain/state/authentication_user_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/check_oidc_is_available_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_oidc_configuration_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_token_oidc_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/authentication_user_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/check_oidc_is_available_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_configuration_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/presentation/login_form_type.dart'; import 'package:tmail_ui_user/features/login/presentation/state/login_state.dart'; import 'package:tmail_ui_user/main/routes/app_routes.dart'; @@ -23,6 +25,7 @@ class LoginController extends GetxController { final AuthorizationInterceptors _authorizationInterceptors; final CheckOIDCIsAvailableInteractor _checkOIDCIsAvailableInteractor; final GetOIDCConfigurationInteractor _getOIDCConfigurationInteractor; + final GetTokenOIDCInteractor _getTokenOIDCInteractor; final TextEditingController urlInputController = TextEditingController(); @@ -32,6 +35,7 @@ class LoginController extends GetxController { this._authorizationInterceptors, this._checkOIDCIsAvailableInteractor, this._getOIDCConfigurationInteractor, + this._getTokenOIDCInteractor, ); var loginState = LoginState(Right(LoginInitAction())).obs; @@ -141,6 +145,35 @@ class LoginController extends GetxController { void _getOIDCConfigurationSuccess(GetOIDCConfigurationSuccess success) { loginState.value = LoginState(Right(success)); + if (currentContext != null) { + _getTokenOIDCAction(currentContext!, success.oidcConfiguration); + } + } + + void _getTokenOIDCAction(BuildContext context, OIDCConfiguration config) async { + loginState.value = LoginState(Right(LoginLoadingAction())); + await _getTokenOIDCInteractor + .execute(config.clientId, config.redirectUrl, config.discoveryUrl, config.scopes) + .then((response) => response.fold( + (failure) { + if (failure is GetTokenOIDCFailure) { + loginState.value = LoginState(Left(failure)); + } else { + loginState.value = LoginState(Left(LoginCanNotGetTokenAction())); + } + }, + (success) { + if (success is GetTokenOIDCSuccess) { + _getTokenOIDCSuccess(success); + } else { + loginState.value = LoginState(Left(LoginCanNotGetTokenAction())); + } + })); + } + + void _getTokenOIDCSuccess(GetTokenOIDCSuccess success) { + log('LoginController::_getTokenOIDCSuccess(): '); + loginState.value = LoginState(Right(success)); } void _loginAction(Uri baseUrl, UserName userName, Password password) async { diff --git a/lib/features/login/presentation/state/login_state.dart b/lib/features/login/presentation/state/login_state.dart index 505ac1ff7..cc412a98e 100644 --- a/lib/features/login/presentation/state/login_state.dart +++ b/lib/features/login/presentation/state/login_state.dart @@ -47,4 +47,10 @@ class LoginMissPasswordAction extends Failure { class LoginCanNotVerifySSOConfigurationAction extends Failure { @override List get props => []; +} + +@immutable +class LoginCanNotGetTokenAction extends Failure { + @override + List get props => []; } \ No newline at end of file diff --git a/lib/main/bindings/network/network_bindings.dart b/lib/main/bindings/network/network_bindings.dart index 8b671e117..995561580 100644 --- a/lib/main/bindings/network/network_bindings.dart +++ b/lib/main/bindings/network/network_bindings.dart @@ -4,10 +4,12 @@ import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:core/core.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:flutter_appauth/flutter_appauth.dart'; import 'package:get/get.dart'; import 'package:jmap_dart_client/http/http_client.dart' as JmapHttpClient; import 'package:tmail_ui_user/features/composer/data/network/composer_api.dart'; import 'package:tmail_ui_user/features/email/data/network/email_api.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_http_client.dart'; import 'package:tmail_ui_user/features/mailbox/data/network/mailbox_api.dart'; import 'package:tmail_ui_user/features/session/data/network/session_api.dart'; import 'package:tmail_ui_user/features/thread/data/network/thread_api.dart'; @@ -57,6 +59,8 @@ class NetworkBindings extends Bindings { Get.find(), Get.find())); Get.put(ComposerAPI(Get.find())); + Get.put(const FlutterAppAuth()); + Get.put(OIDCHttpClient(Get.find(), Get.find())); } void _bindingConnection() { diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index 5f2aa7614..31e41fc31 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1399,4 +1399,10 @@ class AppLocalizations { 'Can not verify SSO configuration, please check with your system administrator', name: 'canNotVerifySSOConfiguration'); } + + String get canNotGetToken { + return Intl.message( + 'Can not get token, please check with your system administrator', + name: 'canNotGetToken'); + } } \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 58a1f5384..bcd890424 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -77,4 +77,7 @@ export 'identity/identity_request_dto.dart'; export 'oidc/response/oidc_response.dart'; export 'oidc/response/oidc_link_dto.dart'; export 'oidc/request/oidc_request.dart'; -export 'oidc/oidc_configuration.dart'; \ No newline at end of file +export 'oidc/oidc_configuration.dart'; +export 'oidc/token_id.dart'; +export 'oidc/token_oidc.dart'; +export 'oidc/token.dart'; \ No newline at end of file diff --git a/model/lib/oidc/token.dart b/model/lib/oidc/token.dart new file mode 100644 index 000000000..2ac83f732 --- /dev/null +++ b/model/lib/oidc/token.dart @@ -0,0 +1,17 @@ + +import 'package:equatable/equatable.dart'; +import 'package:model/oidc/token_id.dart'; + +class Token extends Equatable { + const Token(this.token, this.tokenId); + + final String token; + final TokenId tokenId; + + @override + List get props => [token, tokenId]; +} + +extension TokenExtension on Token { + bool isTokenValid() => token.isNotEmpty && tokenId.uuid.isNotEmpty; +} diff --git a/model/lib/oidc/token_id.dart b/model/lib/oidc/token_id.dart new file mode 100644 index 000000000..809d51710 --- /dev/null +++ b/model/lib/oidc/token_id.dart @@ -0,0 +1,11 @@ + +import 'package:equatable/equatable.dart'; + +class TokenId with EquatableMixin { + final String uuid; + + TokenId(this.uuid); + + @override + List get props => [uuid]; +} diff --git a/model/lib/oidc/token_oidc.dart b/model/lib/oidc/token_oidc.dart new file mode 100644 index 000000000..45f854fed --- /dev/null +++ b/model/lib/oidc/token_oidc.dart @@ -0,0 +1,35 @@ + +import 'package:equatable/equatable.dart'; +import 'package:model/oidc/token.dart'; +import 'package:model/oidc/token_id.dart'; + +class TokenOIDC with EquatableMixin { + + final String token; + final TokenId tokenId; + final DateTime? expiredTime; + final String refreshToken; + + TokenOIDC( + this.token, + this.tokenId, + this.refreshToken, + {this.expiredTime} + ); + + factory TokenOIDC.empty() { + return TokenOIDC('', TokenId(''), ''); + } + + @override + List get props => [token, tokenId, expiredTime, refreshToken]; +} + +extension TokenOIDCExtension on TokenOIDC { + + bool isTokenValid() => token.isNotEmpty && tokenId.uuid.isNotEmpty; + + Token toToken() { + return Token(token, tokenId); + } +} \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 070162751..85cf0a5c9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -153,6 +153,9 @@ dependencies: timeago: 3.2.2 + # OIDC Mobile + flutter_appauth: 4.0.0 + dev_dependencies: flutter_test: sdk: flutter