diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart index cebe659e..fa0bdef0 100644 --- a/userfront/lib/core/services/auth_proxy_service.dart +++ b/userfront/lib/core/services/auth_proxy_service.dart @@ -516,8 +516,13 @@ class AuthProxyService { return jsonDecode(response.body); } else { final errorBody = jsonDecode(response.body); - throw Exception( - errorBody['error'] ?? tr('err.userfront.auth_proxy.oidc_accept'), + final rawDetails = errorBody['details']; + throw AuthProxyException( + errorCode: (errorBody['code'] ?? '').toString(), + message: + (errorBody['error'] ?? tr('err.userfront.auth_proxy.oidc_accept')) + .toString(), + details: rawDetails is Map ? rawDetails : null, ); } } diff --git a/userfront/lib/features/auth/domain/consent_error_routing.dart b/userfront/lib/features/auth/domain/consent_error_routing.dart index 3526352b..93c97503 100644 --- a/userfront/lib/features/auth/domain/consent_error_routing.dart +++ b/userfront/lib/features/auth/domain/consent_error_routing.dart @@ -1,5 +1,29 @@ +import 'dart:convert'; + +import 'package:userfront/core/i18n/locale_utils.dart'; import 'package:userfront/core/services/auth_proxy_service.dart'; -bool shouldRouteConsentErrorToErrorScreen(Object error) { +bool shouldRouteTenantAccessErrorToErrorScreen(Object error) { return error is AuthProxyException && error.errorCode == 'tenant_not_allowed'; } + +bool shouldRouteConsentErrorToErrorScreen(Object error) { + return shouldRouteTenantAccessErrorToErrorScreen(error); +} + +String buildTenantAccessErrorPath(Object error, Uri baseUri) { + final authError = error as AuthProxyException; + final localeCode = + extractLocaleFromPath(baseUri) ?? resolvePreferredLocaleCode(); + return buildLocalizedPath( + localeCode, + Uri( + path: '/error', + queryParameters: { + 'error': authError.errorCode, + 'error_description': authError.message, + if (authError.details != null) 'details': jsonEncode(authError.details), + }, + ), + ); +} diff --git a/userfront/lib/features/auth/presentation/consent_screen.dart b/userfront/lib/features/auth/presentation/consent_screen.dart index cddf6c5a..7e7d44cd 100644 --- a/userfront/lib/features/auth/presentation/consent_screen.dart +++ b/userfront/lib/features/auth/presentation/consent_screen.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:userfront/i18n.dart'; @@ -153,19 +151,7 @@ class _ConsentScreenState extends State { if (!mounted) { return; } - final localeCode = - extractLocaleFromPath(Uri.base) ?? resolvePreferredLocaleCode(); - final target = buildLocalizedPath( - localeCode, - Uri( - path: '/error', - queryParameters: { - 'error': e.errorCode, - 'error_description': e.message, - if (e.details != null) 'details': jsonEncode(e.details), - }, - ), - ); + final target = buildTenantAccessErrorPath(e, Uri.base); context.go(target); return; } diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index f8809bfb..83931cef 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -17,6 +17,7 @@ import '../../../core/services/oidc_redirect_guard.dart'; import '../../../core/notifiers/auth_notifier.dart'; import '../domain/login_challenge_resolver.dart'; import '../domain/cookie_session_policy.dart'; +import '../domain/consent_error_routing.dart'; import '../domain/login_link_route_policy.dart'; import '../domain/verification_completion_route.dart'; import '../../profile/domain/notifiers/profile_notifier.dart'; @@ -1666,6 +1667,16 @@ class _LoginScreenState extends ConsumerState return; } else {} } catch (e) { + if (e is AuthProxyException && + shouldRouteTenantAccessErrorToErrorScreen(e)) { + final target = buildTenantAccessErrorPath(e, Uri.base); + if (mounted) { + context.go(target); + } else { + webWindow.redirectTo(target); + } + return; + } _showError(tr('msg.userfront.login.oidc_failed')); return; } diff --git a/userfront/test/auth_proxy_service_test.dart b/userfront/test/auth_proxy_service_test.dart index 2bb32714..9d46dba0 100644 --- a/userfront/test/auth_proxy_service_test.dart +++ b/userfront/test/auth_proxy_service_test.dart @@ -153,6 +153,37 @@ void main() { }, ); + test( + 'acceptOidcLogin error는 code/message/details를 AuthProxyException으로 보존한다', + () async { + client.enqueueJson({ + 'code': 'tenant_not_allowed', + 'error': 'tenant blocked', + 'details': { + 'allowed_tenants': ['gp'], + }, + }, statusCode: 403); + + await expectLater( + AuthProxyService.acceptOidcLogin('login-challenge', token: 'jwt'), + throwsA( + isA() + .having( + (error) => error.errorCode, + 'code', + 'tenant_not_allowed', + ) + .having((error) => error.message, 'message', 'tenant blocked') + .having( + (error) => error.details?['allowed_tenants'], + 'details', + ['gp'], + ), + ), + ); + }, + ); + test( 'approveQrLogin은 credential mode와 bearer token payload를 지원한다', () async { diff --git a/userfront/test/consent_error_routing_test.dart b/userfront/test/consent_error_routing_test.dart index 4373e3d1..9894ee45 100644 --- a/userfront/test/consent_error_routing_test.dart +++ b/userfront/test/consent_error_routing_test.dart @@ -20,4 +20,33 @@ void main() { expect(shouldRouteConsentErrorToErrorScreen(error), isFalse); }); + + test('tenant_not_allowed auth error also routes to error screen', () { + const error = AuthProxyException( + errorCode: 'tenant_not_allowed', + message: '허용되지 않은 테넌트입니다.', + ); + + expect(shouldRouteTenantAccessErrorToErrorScreen(error), isTrue); + }); + + test('buildTenantAccessErrorPath builds userfront error route', () { + const error = AuthProxyException( + errorCode: 'tenant_not_allowed', + message: '허용되지 않은 테넌트입니다.', + details: { + 'allowed_tenants': ['tenant-a'], + }, + ); + + final target = buildTenantAccessErrorPath( + error, + Uri.parse('https://sso-test.hmac.kr/ko?login_challenge=abc'), + ); + + expect(target, contains('/error?')); + expect(target, contains('error=tenant_not_allowed')); + expect(target, contains('error_description=')); + expect(target, contains('details=')); + }); }