diff --git a/android/app/build.gradle b/android/app/build.gradle index 100dbe313..735388baf 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -40,6 +40,9 @@ android { versionName flutterVersionName // The number of method references in a .dex file cannot exceed 64K multiDexEnabled true + manifestPlaceholders = [ + 'appAuthRedirectScheme': 'teammail.mobile' + ] } buildTypes { diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index ed1e2c0ee..74be58554 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -16,6 +16,11 @@ + + + + + CFBundleURLSchemes ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER) + 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 3b80f5259..1f474497b 100644 --- a/lib/features/login/data/datasource/authentication_oidc_datasource.dart +++ b/lib/features/login/data/datasource/authentication_oidc_datasource.dart @@ -3,4 +3,6 @@ import 'package:model/model.dart'; abstract class AuthenticationOIDCDataSource { Future checkOIDCIsAvailable(OIDCRequest oidcRequest); + + Future getOIDCConfiguration(Uri baseUri); } \ 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 df52f433d..05e4dba31 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 @@ -17,4 +17,13 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { throw error; }); } + + @override + Future getOIDCConfiguration(Uri baseUri) { + return Future.sync(() async { + return await _oidcHttpClient.getOIDCConfiguration(baseUri); + }).catchError((error) { + throw error; + }); + } } \ 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 b4cde1600..142b6feaa 100644 --- a/lib/features/login/data/network/oidc_http_client.dart +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -29,4 +29,8 @@ class OIDCHttpClient { return OIDCResponse.fromJson(jsonDecode(result)); } } + + Future getOIDCConfiguration(Uri baseUri) async { + return OIDCConfiguration.initial(); + } } \ 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 b6c982e51..2de8dcb19 100644 --- a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart @@ -1,3 +1,4 @@ +import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.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'; @@ -11,4 +12,9 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { Future checkOIDCIsAvailable(OIDCRequest oidcRequest) { return _oidcDataSource.checkOIDCIsAvailable(oidcRequest); } + + @override + Future getOIDCConfiguration(Uri baseUri) { + return _oidcDataSource.getOIDCConfiguration(baseUri); + } } \ 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 0ec51e61f..b52915522 100644 --- a/lib/features/login/domain/repository/authentication_oidc_repository.dart +++ b/lib/features/login/domain/repository/authentication_oidc_repository.dart @@ -1,6 +1,9 @@ +import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; abstract class AuthenticationOIDCRepository { Future checkOIDCIsAvailable(OIDCRequest oidcRequest); + + Future getOIDCConfiguration(Uri baseUri); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_oidc_configuration_state.dart b/lib/features/login/domain/state/get_oidc_configuration_state.dart new file mode 100644 index 000000000..135a80243 --- /dev/null +++ b/lib/features/login/domain/state/get_oidc_configuration_state.dart @@ -0,0 +1,21 @@ +import 'package:core/core.dart'; +import 'package:model/model.dart'; + +class GetOIDCConfigurationSuccess extends UIState { + + final OIDCConfiguration oidcConfiguration; + + GetOIDCConfigurationSuccess(this.oidcConfiguration); + + @override + List get props => [oidcConfiguration]; +} + +class GetOIDCConfigurationFailure extends FeatureFailure { + final dynamic exception; + + GetOIDCConfigurationFailure(this.exception); + + @override + List get props => [exception]; +} \ No newline at end of file diff --git a/lib/features/login/domain/usecases/get_oidc_configuration_interactor.dart b/lib/features/login/domain/usecases/get_oidc_configuration_interactor.dart new file mode 100644 index 000000000..3664eb684 --- /dev/null +++ b/lib/features/login/domain/usecases/get_oidc_configuration_interactor.dart @@ -0,0 +1,21 @@ +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_oidc_configuration_state.dart'; + +class GetOIDCConfigurationInteractor { + final AuthenticationOIDCRepository _oidcRepository; + + GetOIDCConfigurationInteractor(this._oidcRepository); + + Future> execute(Uri baseUri) async { + try { + final oidcConfiguration = await _oidcRepository.getOIDCConfiguration(baseUri); + log('GetOIDCConfigurationInteractor::execute(): oidcConfiguration: $oidcConfiguration'); + return Right(GetOIDCConfigurationSuccess(oidcConfiguration)); + } catch (e) { + log('GetOIDCConfigurationInteractor::execute(): ERROR: $e'); + return Left(GetOIDCConfigurationFailure(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 2af56b079..bed91c858 100644 --- a/lib/features/login/presentation/base_login_view.dart +++ b/lib/features/login/presentation/base_login_view.dart @@ -6,6 +6,7 @@ import 'package:core/presentation/views/text/text_field_builder.dart'; 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/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'; @@ -35,6 +36,9 @@ abstract class BaseLoginView extends GetWidget { return AppLocalizations.of(context).requiredEmail; } else if (failure is LoginMissPasswordAction) { return AppLocalizations.of(context).requiredPassword; + } else if (failure is GetOIDCConfigurationFailure + || failure is LoginCanNotVerifySSOConfigurationAction) { + return AppLocalizations.of(context).canNotVerifySSOConfiguration; } 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 ef85aaac6..bd1bf22d7 100644 --- a/lib/features/login/presentation/login_bindings.dart +++ b/lib/features/login/presentation/login_bindings.dart @@ -15,6 +15,7 @@ import 'package:tmail_ui_user/features/login/domain/repository/authentication_re import 'package:tmail_ui_user/features/login/domain/repository/credential_repository.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/presentation/login_controller.dart'; class LoginBindings extends BaseBindings { @@ -32,6 +33,7 @@ class LoginBindings extends BaseBindings { Get.find(), Get.find(), Get.find(), + Get.find(), )); } @@ -56,6 +58,9 @@ class LoginBindings extends BaseBindings { Get.lazyPut(() => CheckOIDCIsAvailableInteractor( Get.find(), )); + Get.lazyPut(() => GetOIDCConfigurationInteractor( + Get.find(), + )); } @override diff --git a/lib/features/login/presentation/login_controller.dart b/lib/features/login/presentation/login_controller.dart index d4bae0319..f9a42fc57 100644 --- a/lib/features/login/presentation/login_controller.dart +++ b/lib/features/login/presentation/login_controller.dart @@ -6,8 +6,10 @@ import 'package:get/get.dart'; 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/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/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'; @@ -20,6 +22,7 @@ class LoginController extends GetxController { final DynamicUrlInterceptors _dynamicUrlInterceptors; final AuthorizationInterceptors _authorizationInterceptors; final CheckOIDCIsAvailableInteractor _checkOIDCIsAvailableInteractor; + final GetOIDCConfigurationInteractor _getOIDCConfigurationInteractor; final TextEditingController urlInputController = TextEditingController(); @@ -28,6 +31,7 @@ class LoginController extends GetxController { this._dynamicUrlInterceptors, this._authorizationInterceptors, this._checkOIDCIsAvailableInteractor, + this._getOIDCConfigurationInteractor, ); var loginState = LoginState(Right(LoginInitAction())).obs; @@ -61,13 +65,13 @@ class LoginController extends GetxController { void _checkOIDCIsAvailable() async { final baseUri = BuildUtils.isWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); - + log('LoginController::_checkOIDCIsAvailable(): baseUri: $baseUri'); if (baseUri == null) { loginState.value = LoginState(Left(LoginMissUrlAction())); } else { loginState.value = LoginState(Right(LoginLoadingAction())); await _checkOIDCIsAvailableInteractor - .execute(OIDCRequest(baseUrl: baseUri.path)) + .execute(OIDCRequest(baseUrl: baseUri.origin)) .then((response) => response.fold( (failure) => _showFormLoginWithCredentialAction(), (success) => success is CheckOIDCIsAvailableSuccess @@ -93,7 +97,12 @@ class LoginController extends GetxController { void handleLoginPressed() { if (loginFormType.value == LoginFormType.ssoForm) { - + final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); + if (baseUri != null) { + _getOIDCConfiguration(baseUri); + } else { + loginState.value = LoginState(Left(LoginMissUrlAction())); + } } else { final baseUri = kIsWeb ? _parseUri(AppConfig.baseUrl) : _parseUri(_urlText); final userName = _parseUserName(_userNameText); @@ -110,6 +119,30 @@ class LoginController extends GetxController { } } + void _getOIDCConfiguration(Uri baseUri) async { + loginState.value = LoginState(Right(LoginLoadingAction())); + await _getOIDCConfigurationInteractor.execute(baseUri) + .then((response) => response.fold( + (failure) { + if (failure is GetOIDCConfigurationFailure) { + loginState.value = LoginState(Left(failure)); + } else { + loginState.value = LoginState(Left(LoginCanNotVerifySSOConfigurationAction())); + } + }, + (success) { + if (success is GetOIDCConfigurationSuccess) { + _getOIDCConfigurationSuccess(success); + } else { + loginState.value = LoginState(Left(LoginCanNotVerifySSOConfigurationAction())); + } + })); + } + + void _getOIDCConfigurationSuccess(GetOIDCConfigurationSuccess success) { + loginState.value = LoginState(Right(success)); + } + void _loginAction(Uri baseUrl, UserName userName, Password password) async { loginState.value = LoginState(Right(LoginLoadingAction())); await _authenticationInteractor.execute(baseUrl, userName, password) @@ -125,11 +158,12 @@ class LoginController extends GetxController { pushAndPop(AppRoutes.SESSION); } - void _loginFailureAction(AuthenticationUserFailure failure) { + void _loginFailureAction(FeatureFailure failure) { loginState.value = LoginState(Left(failure)); } void formatUrl(String url) { + log('LoginController::formatUrl(): $url'); if (url.isValid()) { urlInputController.text = url.removePrefix(); } diff --git a/lib/features/login/presentation/state/login_state.dart b/lib/features/login/presentation/state/login_state.dart index 6876f7a73..505ac1ff7 100644 --- a/lib/features/login/presentation/state/login_state.dart +++ b/lib/features/login/presentation/state/login_state.dart @@ -41,4 +41,10 @@ class LoginMissUsernameAction extends Failure { class LoginMissPasswordAction extends Failure { @override List get props => []; +} + +@immutable +class LoginCanNotVerifySSOConfigurationAction extends Failure { + @override + List get props => []; } \ No newline at end of file diff --git a/lib/main/localizations/app_localizations.dart b/lib/main/localizations/app_localizations.dart index d40cb9e80..5f2aa7614 100644 --- a/lib/main/localizations/app_localizations.dart +++ b/lib/main/localizations/app_localizations.dart @@ -1393,4 +1393,10 @@ class AppLocalizations { 'Sign-in with my SSO account', name: 'loginInputSSOMessage'); } + + String get canNotVerifySSOConfiguration { + return Intl.message( + 'Can not verify SSO configuration, please check with your system administrator', + name: 'canNotVerifySSOConfiguration'); + } } \ No newline at end of file diff --git a/model/lib/model.dart b/model/lib/model.dart index 60723ef97..58a1f5384 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -76,4 +76,5 @@ export 'identity/identity_request_dto.dart'; // OIDC export 'oidc/response/oidc_response.dart'; export 'oidc/response/oidc_link_dto.dart'; -export 'oidc/request/oidc_request.dart'; \ No newline at end of file +export 'oidc/request/oidc_request.dart'; +export 'oidc/oidc_configuration.dart'; \ No newline at end of file diff --git a/model/lib/oidc/oidc_configuration.dart b/model/lib/oidc/oidc_configuration.dart new file mode 100644 index 000000000..68bce8fbe --- /dev/null +++ b/model/lib/oidc/oidc_configuration.dart @@ -0,0 +1,41 @@ + +import 'package:equatable/equatable.dart'; + +class OIDCConfiguration with EquatableMixin { + static const redirectOidc = 'teammail.mobile://oauthredirect'; + static const wellKnownOpenId = '.well-known/openid-configuration'; + + final String authority; + final String clientId; + final String redirectUrl = redirectOidc; + final List scopes; + + OIDCConfiguration({ + required this.authority, + required this.clientId, + required this.scopes + }); + + factory OIDCConfiguration.initial() { + return OIDCConfiguration( + authority: 'https://auth.upn.integration-open-paas.org/auth/realms/mobile', + clientId: 'teammail-mobile', + scopes: ['openid', 'offline_access'] + ); + } + + String get discoveryUrl { + if (authority.endsWith('/')) { + return authority + wellKnownOpenId; + } else { + return authority + '/' + wellKnownOpenId; + } + } + + @override + List get props => [ + authority, + clientId, + scopes + ]; +}