From 43de7edd47c72008975b186d004d92d35aecb32b Mon Sep 17 00:00:00 2001 From: dab246 Date: Wed, 12 Nov 2025 16:25:51 +0700 Subject: [PATCH] TF-4145 Implement get oidc user_info from endpoint of oidc configuration --- .../reloadable/reloadable_controller.dart | 15 ++++++ .../authentication_oidc_datasource.dart | 2 + .../authentication_oidc_datasource_impl.dart | 11 +++++ .../login/data/network/oidc_error.dart | 4 +- .../login/data/network/oidc_http_client.dart | 11 +++++ .../authentication_oidc_repository_impl.dart | 6 +++ .../authentication_oidc_repository.dart | 2 + .../state/get_oidc_user_info_state.dart | 18 +++++++ .../get_oidc_user_info_interactor.dart | 37 ++++++++++++++ .../credential/credential_bindings.dart | 4 ++ lib/main/utils/twake_app_manager.dart | 9 ++++ model/lib/model.dart | 1 + .../response/oidc_discovery_response.dart | 23 +++++++-- model/lib/oidc/response/oidc_user_info.dart | 48 +++++++++++++++++++ 14 files changed, 185 insertions(+), 6 deletions(-) create mode 100644 lib/features/login/domain/state/get_oidc_user_info_state.dart create mode 100644 lib/features/login/domain/usecases/get_oidc_user_info_interactor.dart create mode 100644 model/lib/oidc/response/oidc_user_info.dart diff --git a/lib/features/base/reloadable/reloadable_controller.dart b/lib/features/base/reloadable/reloadable_controller.dart index ec2d0d840..a4d7fa3f1 100644 --- a/lib/features/base/reloadable/reloadable_controller.dart +++ b/lib/features/base/reloadable/reloadable_controller.dart @@ -14,9 +14,11 @@ import 'package:tmail_ui_user/features/home/domain/state/get_session_state.dart' import 'package:tmail_ui_user/features/home/domain/usecases/get_session_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_authenticated_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_credential_state.dart'; +import 'package:tmail_ui_user/features/login/domain/state/get_oidc_user_info_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/get_stored_token_oidc_state.dart'; import 'package:tmail_ui_user/features/login/domain/state/update_authentication_account_state.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_account_interactor.dart'; +import 'package:tmail_ui_user/features/login/domain/usecases/get_oidc_user_info_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/update_account_cache_interactor.dart'; import 'package:tmail_ui_user/features/manage_account/presentation/vacation/vacation_interactors_bindings.dart'; import 'package:tmail_ui_user/main/error/capability_validator.dart'; @@ -26,6 +28,7 @@ abstract class ReloadableController extends BaseController { final GetSessionInteractor getSessionInteractor = Get.find(); final GetAuthenticatedAccountInteractor getAuthenticatedAccountInteractor = Get.find(); final UpdateAccountCacheInteractor _updateAccountCacheInteractor = Get.find(); + final GetOidcUserInfoInteractor _getOidcUserInfoInteractor = Get.find(); @override void handleFailureViewState(Failure failure) { @@ -40,6 +43,8 @@ abstract class ReloadableController extends BaseController { _handleUpdateAccountCacheCompleted( session: failure.session, apiUrl: failure.apiUrl); + } else if (failure is GetOidcUserInfoFailure) { + twakeAppManager.clearOidcUserInfo(); } else { super.handleFailureViewState(failure); } @@ -63,6 +68,9 @@ abstract class ReloadableController extends BaseController { _handleUpdateAccountCacheCompleted( session: success.session, apiUrl: success.apiUrl); + } else if (success is GetOidcUserInfoSuccess) { + log('$runtimeType::handleSuccessViewState:GetOidcUserInfoSuccess: OidcUserInfo = ${success.oidcUserInfo.toJson().toString()}'); + twakeAppManager.setOidcUserInfo(success.oidcUserInfo); } else { super.handleSuccessViewState(success); } @@ -118,6 +126,8 @@ abstract class ReloadableController extends BaseController { if (tokenOIDC != null && oidcConfiguration != null) { authorizationInterceptors.setTokenAndAuthorityOidc(newToken: tokenOIDC, newConfig: oidcConfiguration); authorizationIsolateInterceptors.setTokenAndAuthorityOidc(newToken: tokenOIDC, newConfig: oidcConfiguration); + + getOidcUserInfo(oidcConfiguration); } } @@ -125,6 +135,10 @@ abstract class ReloadableController extends BaseController { consumeState(getSessionInteractor.execute()); } + void getOidcUserInfo(OIDCConfiguration oidcConfiguration) { + consumeState(_getOidcUserInfoInteractor.execute(oidcConfiguration)); + } + void handleGetSessionFailure(GetSessionFailure failure) { if (failure.exception is! BadCredentialsException) { toastManager.showMessageFailure(failure); @@ -176,6 +190,7 @@ abstract class ReloadableController extends BaseController { newConfig: oidcConfiguration, ); + getOidcUserInfo(oidcConfiguration); getSessionAction(); } } \ No newline at end of file diff --git a/lib/features/login/data/datasource/authentication_oidc_datasource.dart b/lib/features/login/data/datasource/authentication_oidc_datasource.dart index 434a8a43a..6083cf350 100644 --- a/lib/features/login/data/datasource/authentication_oidc_datasource.dart +++ b/lib/features/login/data/datasource/authentication_oidc_datasource.dart @@ -46,4 +46,6 @@ abstract class AuthenticationOIDCDataSource { Future getAuthenticationInfo(); Future removeAuthDestinationUrl(); + + Future fetchUserInfo(String userInfoEndpoint); } \ 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 5f951c54b..0132c85b4 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 @@ -2,6 +2,7 @@ import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/response/oidc_response.dart'; +import 'package:model/oidc/response/oidc_user_info.dart'; import 'package:model/oidc/token_id.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/caching/utils/session_storage_manager.dart'; @@ -174,4 +175,14 @@ class AuthenticationOIDCDataSourceImpl extends AuthenticationOIDCDataSource { ); }).catchError(_cacheExceptionThrower.throwException); } + + @override + Future fetchUserInfo(String userInfoEndpoint) { + return Future.sync(() async { + return await _oidcHttpClient.fetchUserInfo(userInfoEndpoint); + }).catchError((error, stackTrace) async { + await _exceptionThrower.throwException(error, stackTrace); + throw error; + }); + } } \ No newline at end of file diff --git a/lib/features/login/data/network/oidc_error.dart b/lib/features/login/data/network/oidc_error.dart index c529a2590..bd9f1a6af 100644 --- a/lib/features/login/data/network/oidc_error.dart +++ b/lib/features/login/data/network/oidc_error.dart @@ -4,4 +4,6 @@ class CanNotFoundOIDCLinks implements Exception {} class CanNotFindToken implements Exception {} -class CanRetryOIDCException implements Exception {} \ No newline at end of file +class CanRetryOIDCException implements Exception {} + +class NotFoundUserInfoEndpointException implements Exception {} \ 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 61cc3c881..bf0064f85 100644 --- a/lib/features/login/data/network/oidc_http_client.dart +++ b/lib/features/login/data/network/oidc_http_client.dart @@ -10,6 +10,7 @@ import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/response/oidc_response.dart'; +import 'package:model/oidc/response/oidc_user_info.dart'; import 'package:tmail_ui_user/features/login/data/extensions/service_path_extension.dart'; import 'package:tmail_ui_user/features/login/data/network/config/oidc_constant.dart'; import 'package:tmail_ui_user/features/login/data/network/endpoint.dart'; @@ -84,4 +85,14 @@ class OIDCHttpClient { return OIDCDiscoveryResponse.fromJson(jsonDecode(result)); } } + + Future fetchUserInfo(String userInfoEndpoint) async { + final result = await _dioClient.get(userInfoEndpoint); + + if (result is Map) { + return OidcUserInfo.fromJson(result); + } else { + return OidcUserInfo.fromJson(jsonDecode(result)); + } + } } \ 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 0f83295bd..f0ee0f7d8 100644 --- a/lib/features/login/data/repository/authentication_oidc_repository_impl.dart +++ b/lib/features/login/data/repository/authentication_oidc_repository_impl.dart @@ -2,6 +2,7 @@ import 'package:model/oidc/oidc_configuration.dart'; import 'package:model/oidc/request/oidc_request.dart'; import 'package:model/oidc/response/oidc_discovery_response.dart'; import 'package:model/oidc/response/oidc_response.dart'; +import 'package:model/oidc/response/oidc_user_info.dart'; import 'package:model/oidc/token_id.dart'; import 'package:model/oidc/token_oidc.dart'; import 'package:tmail_ui_user/features/login/data/datasource/authentication_oidc_datasource.dart'; @@ -110,4 +111,9 @@ class AuthenticationOIDCRepositoryImpl extends AuthenticationOIDCRepository { Future removeAuthDestinationUrl() { return _oidcDataSource.removeAuthDestinationUrl(); } + + @override + Future fetchUserInfo(String userInfoEndpoint) { + return _oidcDataSource.fetchUserInfo(userInfoEndpoint); + } } \ 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 5f5893134..17093c5b6 100644 --- a/lib/features/login/domain/repository/authentication_oidc_repository.dart +++ b/lib/features/login/domain/repository/authentication_oidc_repository.dart @@ -46,4 +46,6 @@ abstract class AuthenticationOIDCRepository { Future getAuthenticationInfo(); Future removeAuthDestinationUrl(); + + Future fetchUserInfo(String userInfoEndpoint); } \ No newline at end of file diff --git a/lib/features/login/domain/state/get_oidc_user_info_state.dart b/lib/features/login/domain/state/get_oidc_user_info_state.dart new file mode 100644 index 000000000..a8f7cb8a6 --- /dev/null +++ b/lib/features/login/domain/state/get_oidc_user_info_state.dart @@ -0,0 +1,18 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:model/oidc/response/oidc_user_info.dart'; + +class GettingOidcUserInfo extends LoadingState {} + +class GetOidcUserInfoSuccess extends UIState { + final OidcUserInfo oidcUserInfo; + + GetOidcUserInfoSuccess(this.oidcUserInfo); + + @override + List get props => [oidcUserInfo]; +} + +class GetOidcUserInfoFailure extends FeatureFailure { + GetOidcUserInfoFailure(dynamic exception) : super(exception: exception); +} diff --git a/lib/features/login/domain/usecases/get_oidc_user_info_interactor.dart b/lib/features/login/domain/usecases/get_oidc_user_info_interactor.dart new file mode 100644 index 000000000..fcbaa3af5 --- /dev/null +++ b/lib/features/login/domain/usecases/get_oidc_user_info_interactor.dart @@ -0,0 +1,37 @@ +import 'package:core/presentation/state/failure.dart'; +import 'package:core/presentation/state/success.dart'; +import 'package:dartz/dartz.dart'; +import 'package:model/oidc/oidc_configuration.dart'; +import 'package:tmail_ui_user/features/login/data/network/oidc_error.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_user_info_state.dart'; + +class GetOidcUserInfoInteractor { + final AuthenticationOIDCRepository _authenticationOIDCRepository; + + GetOidcUserInfoInteractor(this._authenticationOIDCRepository); + + Stream> execute(OIDCConfiguration config) async* { + try { + yield Right(GettingOidcUserInfo()); + + final oidcDiscoveryResponse = + await _authenticationOIDCRepository.discoverOIDC(config); + + final userInfoEndpoint = oidcDiscoveryResponse.userInfoEndpoint; + + if (userInfoEndpoint != null) { + final oidcUserInfo = + await _authenticationOIDCRepository.fetchUserInfo(userInfoEndpoint); + + yield Right(GetOidcUserInfoSuccess(oidcUserInfo)); + } else { + yield Left( + GetOidcUserInfoFailure(NotFoundUserInfoEndpointException()), + ); + } + } catch (e) { + yield Left(GetOidcUserInfoFailure(e)); + } + } +} diff --git a/lib/main/bindings/credential/credential_bindings.dart b/lib/main/bindings/credential/credential_bindings.dart index f78fc7ff6..be2b202d6 100644 --- a/lib/main/bindings/credential/credential_bindings.dart +++ b/lib/main/bindings/credential/credential_bindings.dart @@ -31,6 +31,7 @@ import 'package:tmail_ui_user/features/login/domain/usecases/get_authenticated_a import 'package:tmail_ui_user/features/login/domain/usecases/get_authentication_info_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_credential_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_oidc_user_info_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_oidc_configuration_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_stored_token_oidc_interactor.dart'; import 'package:tmail_ui_user/features/login/domain/usecases/get_token_oidc_interactor.dart'; @@ -93,6 +94,9 @@ class CredentialBindings extends InteractorsBindings { Get.put(RemoveAuthDestinationUrlInteractor( Get.find(), )); + Get.put(GetOidcUserInfoInteractor( + Get.find(), + )); } @override diff --git a/lib/main/utils/twake_app_manager.dart b/lib/main/utils/twake_app_manager.dart index fc7dff513..c8b1f19a7 100644 --- a/lib/main/utils/twake_app_manager.dart +++ b/lib/main/utils/twake_app_manager.dart @@ -1,7 +1,10 @@ +import 'package:model/oidc/response/oidc_user_info.dart'; + class TwakeAppManager { bool _hasComposer = false; bool _isExecutingBeforeReconnect = false; + OidcUserInfo? _oidcUserInfo; void setHasComposer(bool value) => _hasComposer = value; @@ -10,4 +13,10 @@ class TwakeAppManager { void setExecutingBeforeReconnect(bool value) => _isExecutingBeforeReconnect = value; bool get isExecutingBeforeReconnect => _isExecutingBeforeReconnect; + + void setOidcUserInfo(OidcUserInfo value) => _oidcUserInfo = value; + + void clearOidcUserInfo() => _oidcUserInfo = null; + + OidcUserInfo? get oidcUserInfo => _oidcUserInfo; } diff --git a/model/lib/model.dart b/model/lib/model.dart index 3a8d4d188..77fd5e9a3 100644 --- a/model/lib/model.dart +++ b/model/lib/model.dart @@ -78,6 +78,7 @@ export 'oidc/request/oidc_request.dart'; export 'oidc/response/oidc_discovery_response.dart'; export 'oidc/response/oidc_link_dto.dart'; export 'oidc/response/oidc_response.dart'; +export 'oidc/response/oidc_user_info.dart'; export 'oidc/token_id.dart'; export 'oidc/token_oidc.dart'; // Upload diff --git a/model/lib/oidc/response/oidc_discovery_response.dart b/model/lib/oidc/response/oidc_discovery_response.dart index 76700cd67..49c044809 100644 --- a/model/lib/oidc/response/oidc_discovery_response.dart +++ b/model/lib/oidc/response/oidc_discovery_response.dart @@ -5,7 +5,6 @@ part 'oidc_discovery_response.g.dart'; @JsonSerializable(explicitToJson: true, includeIfNull: false) class OIDCDiscoveryResponse with EquatableMixin { - @JsonKey(name: 'authorization_endpoint') final String? authorizationEndpoint; @@ -15,12 +14,26 @@ class OIDCDiscoveryResponse with EquatableMixin { @JsonKey(name: 'end_session_endpoint') final String? endSessionEndpoint; - OIDCDiscoveryResponse(this.authorizationEndpoint, this.tokenEndpoint, this.endSessionEndpoint); + @JsonKey(name: 'userinfo_endpoint') + final String? userInfoEndpoint; - factory OIDCDiscoveryResponse.fromJson(Map json) => _$OIDCDiscoveryResponseFromJson(json); + OIDCDiscoveryResponse( + this.authorizationEndpoint, + this.tokenEndpoint, + this.endSessionEndpoint, + this.userInfoEndpoint, + ); + + factory OIDCDiscoveryResponse.fromJson(Map json) => + _$OIDCDiscoveryResponseFromJson(json); Map toJson() => _$OIDCDiscoveryResponseToJson(this); @override - List get props => [authorizationEndpoint, tokenEndpoint, endSessionEndpoint]; -} \ No newline at end of file + List get props => [ + authorizationEndpoint, + tokenEndpoint, + endSessionEndpoint, + userInfoEndpoint, + ]; +} diff --git a/model/lib/oidc/response/oidc_user_info.dart b/model/lib/oidc/response/oidc_user_info.dart new file mode 100644 index 000000000..2f202d6af --- /dev/null +++ b/model/lib/oidc/response/oidc_user_info.dart @@ -0,0 +1,48 @@ +import 'package:equatable/equatable.dart'; +import 'package:json_annotation/json_annotation.dart'; + +part 'oidc_user_info.g.dart'; + +@JsonSerializable(explicitToJson: true, includeIfNull: false) +class OidcUserInfo with EquatableMixin { + @JsonKey(name: 'sid') + final String? id; + + final String? email; + + final String? sub; + + final String? name; + + @JsonKey(name: 'given_name') + final String? givenName; + + @JsonKey(name: 'family_name') + final String? familyName; + + const OidcUserInfo({ + this.id, + this.sub, + this.name, + this.givenName, + this.familyName, + this.email, + }); + + /// Factory for creating object from JSON + factory OidcUserInfo.fromJson(Map json) => + _$OidcUserInfoFromJson(json); + + /// Convert object to JSON map + Map toJson() => _$OidcUserInfoToJson(this); + + @override + List get props => [ + id, + sub, + name, + givenName, + familyName, + email, + ]; +}