diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index d65803ac..1358a7b7 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -1629,6 +1629,9 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error { logOidcRedirectSummary("password_login", acceptResp.RedirectTo) return c.JSON(fiber.Map{ "redirectTo": acceptResp.RedirectTo, + "sessionJwt": authInfo.SessionToken.JWT, + "status": "ok", + "provider": h.IdpProvider.Name(), }) } // --- OIDC 로그인 흐름 처리 끝 --- diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart index 70a555d4..29e78ede 100644 --- a/userfront/lib/core/services/auth_proxy_service.dart +++ b/userfront/lib/core/services/auth_proxy_service.dart @@ -287,18 +287,26 @@ class AuthProxyService { final url = Uri.parse( '$_baseUrl/api/v1/auth/consent', ).replace(queryParameters: {'consent_challenge': consentChallenge}); - final response = await http.get( - url, - headers: {'Content-Type': 'application/json'}, - ); - - if (response.statusCode == 200) { - return jsonDecode(response.body); - } else { - final errorBody = jsonDecode(response.body); - throw Exception( - errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_fetch'), + final client = createHttpClient(withCredentials: true); + try { + final response = await client.get( + url, + headers: {'Content-Type': 'application/json'}, ); + + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else { + final errorBody = jsonDecode(response.body); + throw Exception( + errorBody['error'] ?? + tr( + 'err.userfront.auth_proxy.consent_fetch', + ), + ); + } + } finally { + client.close(); } } @@ -312,19 +320,27 @@ class AuthProxyService { body['grant_scope'] = grantScope; } - final response = await http.post( - url, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode(body), - ); - - if (response.statusCode == 200) { - return jsonDecode(response.body); - } else { - final errorBody = jsonDecode(response.body); - throw Exception( - errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_accept'), + final client = createHttpClient(withCredentials: true); + try { + final response = await client.post( + url, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), ); + + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else { + final errorBody = jsonDecode(response.body); + throw Exception( + errorBody['error'] ?? + tr( + 'err.userfront.auth_proxy.consent_accept', + ), + ); + } + } finally { + client.close(); } } @@ -334,19 +350,27 @@ class AuthProxyService { final url = Uri.parse('$_baseUrl/api/v1/auth/consent/reject'); final body = {'consent_challenge': consentChallenge}; - final response = await http.post( - url, - headers: {'Content-Type': 'application/json'}, - body: jsonEncode(body), - ); - - if (response.statusCode == 200) { - return jsonDecode(response.body); - } else { - final errorBody = jsonDecode(response.body); - throw Exception( - errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_reject'), + final client = createHttpClient(withCredentials: true); + try { + final response = await client.post( + url, + headers: {'Content-Type': 'application/json'}, + body: jsonEncode(body), ); + + if (response.statusCode == 200) { + return jsonDecode(response.body); + } else { + final errorBody = jsonDecode(response.body); + throw Exception( + errorBody['error'] ?? + tr( + 'err.userfront.auth_proxy.consent_reject', + ), + ); + } + } finally { + client.close(); } } diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index db8e0eac..93ac1b0c 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -709,7 +709,7 @@ class _LoginScreenState extends ConsumerState ); return; } - _markVerificationApproved(approvedMessage, actionPath: actionPath); + _onLoginSuccess(jwt, provider: res['provider'] as String?); return; } @@ -777,14 +777,7 @@ class _LoginScreenState extends ConsumerState _markVerificationApproved(approvedMessage, actionPath: actionPath); return; } - _markVerificationApproved( - linkLoginMessage, - title: tr('ui.userfront.login.link.title'), - pageTitle: tr('ui.userfront.login.link.page_title'), - actionLabel: tr('ui.userfront.login.link.action_label'), - actionPath: '/signin', - autoRedirect: true, - ); + _onLoginSuccess(jwt, provider: res['provider'] as String?); return; } @@ -844,7 +837,7 @@ class _LoginScreenState extends ConsumerState _markVerificationApproved(approvedMessage, actionPath: actionPath); return; } - _completeLoginFromToken(jwt, provider: res['provider'] as String?); + _onLoginSuccess(jwt, provider: res['provider'] as String?); return; } @@ -879,6 +872,7 @@ class _LoginScreenState extends ConsumerState } Future _handlePasswordLogin() async { + print("[Auth] _handlePasswordLogin START"); final input = _passwordLoginIdController.text.trim(); final password = _passwordController.text.trim(); if (input.isEmpty || password.isEmpty) { @@ -895,65 +889,29 @@ class _LoginScreenState extends ConsumerState } try { - final challengeResolution = _resolveLoginChallenge(Uri.base); - if (!_hasLoginChallenge && challengeResolution.value != null) { - _loginChallenge = challengeResolution.value; - } - _logLoginChallengeDiagnostics( - phase: 'password_submit', - resolution: challengeResolution, - ); - + print("[Auth] Calling AuthProxyService.loginWithPassword..."); final res = await AuthProxyService.loginWithPassword( loginId, password, loginChallenge: _loginChallenge, ); - final jwtRaw = res['sessionJwt'] ?? res['sessionToken'] ?? res['token']; - final jwt = jwtRaw?.toString(); + print("[Auth] loginWithPassword response: $res"); + + final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token']; final provider = res['provider'] as String?; final redirectTo = res['redirectTo'] as String?; - final hasJwt = jwt != null && jwt.isNotEmpty; - final nextAction = decidePasswordLoginNextAction( - hasLoginChallenge: _hasLoginChallenge, - redirectTo: redirectTo, - jwt: jwt, - ); - debugPrint( - "[Auth] Password login outcome: has_login_challenge=$_hasLoginChallenge, next_action=$nextAction, has_jwt=$hasJwt", - ); - if (!_hasLoginChallenge) { - debugPrint( - "[Auth] WARNING: password login proceeded without login_challenge; treated as local login flow", - ); - } - - switch (nextAction) { - case PasswordLoginNextAction.redirectToOidc: - _redirectToOidcTarget(redirectTo!, source: 'password_login'); - return; - case PasswordLoginNextAction.acceptOidc: - final accepted = await _acceptOidcLoginAndRedirect( - token: hasJwt ? jwt : null, - ); - if (accepted) { - return; - } - if (mounted) { - _showError(tr('msg.userfront.login.oidc_failed')); - } - return; - case PasswordLoginNextAction.localLogin: - _onLoginSuccess(jwt!, provider: provider); - return; - case PasswordLoginNextAction.invalid: - if (mounted) { - _showError(tr('msg.userfront.login.password.failed')); - } - return; + if (jwt != null) { + print("[Auth] JWT found, calling _onLoginSuccess. RedirectTo: $redirectTo"); + _onLoginSuccess(jwt, provider: provider, redirectTo: redirectTo); + } else if (redirectTo != null && redirectTo.isNotEmpty) { + print("[Auth] Only redirectTo found. Redirecting..."); + webWindow.redirectTo(redirectTo); + } else { + print("[Auth] No JWT and no redirectTo found."); } } catch (e) { + print("[Auth] _handlePasswordLogin Error: $e"); if (e.toString().contains("User not registered")) { _showUnregisteredDialog(); } else { @@ -1175,57 +1133,103 @@ class _LoginScreenState extends ConsumerState } } - void _onLoginSuccess(String token, {String? provider}) async { - if (!mounted) return; - - _logTokenDetails(token); - - final providerName = provider ?? AuthTokenStore.getProvider(); - - AuthTokenStore.setToken(token, provider: providerName); - AuthTokenStore.clearPendingProvider(); - _dismissOverlays(); - + Future _onLoginSuccess(String token, {String? provider, String? redirectTo}) async { + print("[Auth] _onLoginSuccess ENTRY. RedirectTo: $redirectTo, Token len: ${token.length}"); + try { - await ref.read(profileProvider.notifier).loadProfile(); - } catch (e) { - debugPrint("[Auth] Failed to pre-fetch profile: $e"); - } - - if (_hasLoginChallenge) { - try { - final accepted = await _acceptOidcLoginAndRedirect(token: token); - if (accepted) { + if (!mounted) { + print("[Auth] _onLoginSuccess: Not mounted, returning."); return; + } + + // [Priority 1] Immediate External Redirection + if (redirectTo != null && redirectTo.isNotEmpty) { + print("[Auth] _onLoginSuccess: Has redirectTo. Saving token and redirecting..."); + try { + final providerName = provider ?? AuthTokenStore.getProvider(); + print("[Auth] _onLoginSuccess: Provider resolved: $providerName"); + AuthTokenStore.setToken(token, provider: providerName); + print("[Auth] _onLoginSuccess: Token saved to store."); + } catch (stErr) { + print("[Auth] _onLoginSuccess: FAILED to save token: $stErr"); } - if (mounted) { - _showError(tr('msg.userfront.login.oidc_failed')); - } - return; - } catch (e) { - _showError(tr('msg.userfront.login.oidc_failed')); + + print("[Auth] Calling webWindow.redirectTo: $redirectTo"); + webWindow.redirectTo(redirectTo); // Removed await as it's void return; } - } - final uri = Uri.base; - final redirectParam = - uri.queryParameters['redirect_uri'] ?? - uri.queryParameters['redirect_url']; - final hasRedirectParam = redirectParam != null && redirectParam.isNotEmpty; + // [Priority 2] OIDC Challenge Handling + if (_loginChallenge != null && _loginChallenge!.isNotEmpty) { + print("[Auth] _onLoginSuccess: Has loginChallenge. Attempting auto-accept..."); + try { + // Save token first, it's needed for acceptance + final providerName = provider ?? AuthTokenStore.getProvider(); + AuthTokenStore.setToken(token, provider: providerName); + print("[Auth] _onLoginSuccess: Token saved for auto-accept."); + + final res = await AuthProxyService.acceptOidcLogin( + _loginChallenge!, + token: token, + ); + final nextRedirectTo = res['redirectTo'] as String?; + print("[Auth] Auto-accept response: $res"); + + if (nextRedirectTo != null && nextRedirectTo.isNotEmpty) { + print("[Auth] OIDC login accepted. Redirecting to: $nextRedirectTo"); + webWindow.redirectTo(nextRedirectTo); // Removed await + return; + } else { + print("[Auth] Auto-accept successful but no redirectTo provided."); + } + } catch (e) { + print("[Auth] Auto-accept failed: $e"); + _showError( + tr( + 'msg.userfront.login.oidc_failed', + ), + ); + return; + } + } - if (WebAuthIntegration.isPopup() || hasRedirectParam) { - debugPrint( - "[Auth] External integration detected (popup or redirect). Notifying...", - ); - WebAuthIntegration.sendLoginSuccess(token); - return; - } + print("[Auth] _onLoginSuccess: Standard Login Flow"); + _logTokenDetails(token); - debugPrint("[Auth] Login success. Navigating to root."); - AuthNotifier.instance.notify(); - if (mounted) { - context.go('/'); + final providerName = provider ?? AuthTokenStore.getProvider(); + + AuthTokenStore.setToken(token, provider: providerName); + AuthTokenStore.clearPendingProvider(); + _dismissOverlays(); + + try { + await ref.read(profileProvider.notifier).loadProfile(); + } catch (e) { + print("[Auth] Failed to pre-fetch profile: $e"); + } + + final uri = Uri.base; + final redirectParam = + uri.queryParameters['redirect_uri'] ?? uri.queryParameters['redirect_url']; + final hasRedirectParam = + redirectParam != null && redirectParam.isNotEmpty; + + if (WebAuthIntegration.isPopup() || hasRedirectParam) { + print( + "[Auth] External integration detected (popup or redirect). Notifying...", + ); + WebAuthIntegration.sendLoginSuccess(token); + AuthNotifier.instance.notify(); + return; + } + + print("[Auth] Login success. Navigating to root."); + AuthNotifier.instance.notify(); + if (mounted) { + context.go('/'); + } + } catch (globalErr) { + print("[Auth] CRITICAL ERROR in _onLoginSuccess: $globalErr"); } }