1
0
forked from baron/baron-sso

리다이렉트 책임 단일화 및 인증 흐름 추적 로그 추가

This commit is contained in:
2026-02-19 13:49:35 +09:00
parent 43a4909ddf
commit b43ace8b2d
3 changed files with 168 additions and 137 deletions

View File

@@ -1629,6 +1629,9 @@ func (h *AuthHandler) PasswordLogin(c *fiber.Ctx) error {
logOidcRedirectSummary("password_login", acceptResp.RedirectTo) logOidcRedirectSummary("password_login", acceptResp.RedirectTo)
return c.JSON(fiber.Map{ return c.JSON(fiber.Map{
"redirectTo": acceptResp.RedirectTo, "redirectTo": acceptResp.RedirectTo,
"sessionJwt": authInfo.SessionToken.JWT,
"status": "ok",
"provider": h.IdpProvider.Name(),
}) })
} }
// --- OIDC 로그인 흐름 처리 끝 --- // --- OIDC 로그인 흐름 처리 끝 ---

View File

@@ -287,7 +287,9 @@ class AuthProxyService {
final url = Uri.parse( final url = Uri.parse(
'$_baseUrl/api/v1/auth/consent', '$_baseUrl/api/v1/auth/consent',
).replace(queryParameters: {'consent_challenge': consentChallenge}); ).replace(queryParameters: {'consent_challenge': consentChallenge});
final response = await http.get( final client = createHttpClient(withCredentials: true);
try {
final response = await client.get(
url, url,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
); );
@@ -297,9 +299,15 @@ class AuthProxyService {
} else { } else {
final errorBody = jsonDecode(response.body); final errorBody = jsonDecode(response.body);
throw Exception( throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_fetch'), errorBody['error'] ??
tr(
'err.userfront.auth_proxy.consent_fetch',
),
); );
} }
} finally {
client.close();
}
} }
static Future<Map<String, dynamic>> acceptConsent( static Future<Map<String, dynamic>> acceptConsent(
@@ -312,7 +320,9 @@ class AuthProxyService {
body['grant_scope'] = grantScope; body['grant_scope'] = grantScope;
} }
final response = await http.post( final client = createHttpClient(withCredentials: true);
try {
final response = await client.post(
url, url,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(body), body: jsonEncode(body),
@@ -323,9 +333,15 @@ class AuthProxyService {
} else { } else {
final errorBody = jsonDecode(response.body); final errorBody = jsonDecode(response.body);
throw Exception( throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_accept'), errorBody['error'] ??
tr(
'err.userfront.auth_proxy.consent_accept',
),
); );
} }
} finally {
client.close();
}
} }
static Future<Map<String, dynamic>> rejectConsent( static Future<Map<String, dynamic>> rejectConsent(
@@ -334,7 +350,9 @@ class AuthProxyService {
final url = Uri.parse('$_baseUrl/api/v1/auth/consent/reject'); final url = Uri.parse('$_baseUrl/api/v1/auth/consent/reject');
final body = <String, dynamic>{'consent_challenge': consentChallenge}; final body = <String, dynamic>{'consent_challenge': consentChallenge};
final response = await http.post( final client = createHttpClient(withCredentials: true);
try {
final response = await client.post(
url, url,
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
body: jsonEncode(body), body: jsonEncode(body),
@@ -345,9 +363,15 @@ class AuthProxyService {
} else { } else {
final errorBody = jsonDecode(response.body); final errorBody = jsonDecode(response.body);
throw Exception( throw Exception(
errorBody['error'] ?? tr('err.userfront.auth_proxy.consent_reject'), errorBody['error'] ??
tr(
'err.userfront.auth_proxy.consent_reject',
),
); );
} }
} finally {
client.close();
}
} }
static Future<Map<String, dynamic>> acceptOidcLogin( static Future<Map<String, dynamic>> acceptOidcLogin(

View File

@@ -709,7 +709,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
); );
return; return;
} }
_markVerificationApproved(approvedMessage, actionPath: actionPath); _onLoginSuccess(jwt, provider: res['provider'] as String?);
return; return;
} }
@@ -777,14 +777,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_markVerificationApproved(approvedMessage, actionPath: actionPath); _markVerificationApproved(approvedMessage, actionPath: actionPath);
return; return;
} }
_markVerificationApproved( _onLoginSuccess(jwt, provider: res['provider'] as String?);
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,
);
return; return;
} }
@@ -844,7 +837,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
_markVerificationApproved(approvedMessage, actionPath: actionPath); _markVerificationApproved(approvedMessage, actionPath: actionPath);
return; return;
} }
_completeLoginFromToken(jwt, provider: res['provider'] as String?); _onLoginSuccess(jwt, provider: res['provider'] as String?);
return; return;
} }
@@ -879,6 +872,7 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
} }
Future<void> _handlePasswordLogin() async { Future<void> _handlePasswordLogin() async {
print("[Auth] _handlePasswordLogin START");
final input = _passwordLoginIdController.text.trim(); final input = _passwordLoginIdController.text.trim();
final password = _passwordController.text.trim(); final password = _passwordController.text.trim();
if (input.isEmpty || password.isEmpty) { if (input.isEmpty || password.isEmpty) {
@@ -895,65 +889,29 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
} }
try { try {
final challengeResolution = _resolveLoginChallenge(Uri.base); print("[Auth] Calling AuthProxyService.loginWithPassword...");
if (!_hasLoginChallenge && challengeResolution.value != null) {
_loginChallenge = challengeResolution.value;
}
_logLoginChallengeDiagnostics(
phase: 'password_submit',
resolution: challengeResolution,
);
final res = await AuthProxyService.loginWithPassword( final res = await AuthProxyService.loginWithPassword(
loginId, loginId,
password, password,
loginChallenge: _loginChallenge, loginChallenge: _loginChallenge,
); );
final jwtRaw = res['sessionJwt'] ?? res['sessionToken'] ?? res['token']; print("[Auth] loginWithPassword response: $res");
final jwt = jwtRaw?.toString();
final jwt = res['sessionJwt'] ?? res['sessionToken'] ?? res['token'];
final provider = res['provider'] as String?; final provider = res['provider'] as String?;
final redirectTo = res['redirectTo'] as String?; final redirectTo = res['redirectTo'] as String?;
final hasJwt = jwt != null && jwt.isNotEmpty;
final nextAction = decidePasswordLoginNextAction(
hasLoginChallenge: _hasLoginChallenge,
redirectTo: redirectTo,
jwt: jwt,
);
debugPrint( if (jwt != null) {
"[Auth] Password login outcome: has_login_challenge=$_hasLoginChallenge, next_action=$nextAction, has_jwt=$hasJwt", print("[Auth] JWT found, calling _onLoginSuccess. RedirectTo: $redirectTo");
); _onLoginSuccess(jwt, provider: provider, redirectTo: redirectTo);
if (!_hasLoginChallenge) { } else if (redirectTo != null && redirectTo.isNotEmpty) {
debugPrint( print("[Auth] Only redirectTo found. Redirecting...");
"[Auth] WARNING: password login proceeded without login_challenge; treated as local login flow", webWindow.redirectTo(redirectTo);
); } else {
} print("[Auth] No JWT and no redirectTo found.");
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;
} }
} catch (e) { } catch (e) {
print("[Auth] _handlePasswordLogin Error: $e");
if (e.toString().contains("User not registered")) { if (e.toString().contains("User not registered")) {
_showUnregisteredDialog(); _showUnregisteredDialog();
} else { } else {
@@ -1175,9 +1133,67 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
} }
} }
void _onLoginSuccess(String token, {String? provider}) async { Future<void> _onLoginSuccess(String token, {String? provider, String? redirectTo}) async {
if (!mounted) return; print("[Auth] _onLoginSuccess ENTRY. RedirectTo: $redirectTo, Token len: ${token.length}");
try {
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");
}
print("[Auth] Calling webWindow.redirectTo: $redirectTo");
webWindow.redirectTo(redirectTo); // Removed await as it's void
return;
}
// [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;
}
}
print("[Auth] _onLoginSuccess: Standard Login Flow");
_logTokenDetails(token); _logTokenDetails(token);
final providerName = provider ?? AuthTokenStore.getProvider(); final providerName = provider ?? AuthTokenStore.getProvider();
@@ -1189,44 +1205,32 @@ class _LoginScreenState extends ConsumerState<LoginScreen>
try { try {
await ref.read(profileProvider.notifier).loadProfile(); await ref.read(profileProvider.notifier).loadProfile();
} catch (e) { } catch (e) {
debugPrint("[Auth] Failed to pre-fetch profile: $e"); print("[Auth] Failed to pre-fetch profile: $e");
}
if (_hasLoginChallenge) {
try {
final accepted = await _acceptOidcLoginAndRedirect(token: token);
if (accepted) {
return;
}
if (mounted) {
_showError(tr('msg.userfront.login.oidc_failed'));
}
return;
} catch (e) {
_showError(tr('msg.userfront.login.oidc_failed'));
return;
}
} }
final uri = Uri.base; final uri = Uri.base;
final redirectParam = final redirectParam =
uri.queryParameters['redirect_uri'] ?? uri.queryParameters['redirect_uri'] ?? uri.queryParameters['redirect_url'];
uri.queryParameters['redirect_url']; final hasRedirectParam =
final hasRedirectParam = redirectParam != null && redirectParam.isNotEmpty; redirectParam != null && redirectParam.isNotEmpty;
if (WebAuthIntegration.isPopup() || hasRedirectParam) { if (WebAuthIntegration.isPopup() || hasRedirectParam) {
debugPrint( print(
"[Auth] External integration detected (popup or redirect). Notifying...", "[Auth] External integration detected (popup or redirect). Notifying...",
); );
WebAuthIntegration.sendLoginSuccess(token); WebAuthIntegration.sendLoginSuccess(token);
AuthNotifier.instance.notify();
return; return;
} }
debugPrint("[Auth] Login success. Navigating to root."); print("[Auth] Login success. Navigating to root.");
AuthNotifier.instance.notify(); AuthNotifier.instance.notify();
if (mounted) { if (mounted) {
context.go('/'); context.go('/');
} }
} catch (globalErr) {
print("[Auth] CRITICAL ERROR in _onLoginSuccess: $globalErr");
}
} }
void _showUnregisteredDialog() { void _showUnregisteredDialog() {