diff --git a/userfront-e2e/tests/oidc-login-challenge.spec.ts b/userfront-e2e/tests/oidc-login-challenge.spec.ts new file mode 100644 index 00000000..920a69b4 --- /dev/null +++ b/userfront-e2e/tests/oidc-login-challenge.spec.ts @@ -0,0 +1,81 @@ +import { expect, test, type Page, type Route } from '@playwright/test'; + +async function mockUserfrontApisForRepro( + page: Page, + options: { sessionStatus: number } = { sessionStatus: 401 }, +): Promise { + await page.route('**/api/v1/**', async (route: Route) => { + const requestUrl = new URL(route.request().url()); + const path = requestUrl.pathname; + + if (path.endsWith('/api/v1/user/me')) { + await route.fulfill({ + status: options.sessionStatus, + contentType: 'application/json', + body: JSON.stringify({ error: 'unauthorized' }), + }); + return; + } + + if (path.endsWith('/api/v1/client-log')) { + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({ ok: true }), + }); + return; + } + + // Default mock for other APIs + await route.fulfill({ + status: 200, + contentType: 'application/json', + body: JSON.stringify({}), + }); + }); +} + +test.describe('Issue #345 Reproduction (Log-based Validation)', () => { + test('비로그인 상태에서 login_challenge와 함께 signin 진입 시 루프 없이 로그가 정상 출력되어야 한다', async ({ page }) => { + const logs: string[] = []; + page.on('console', msg => { + const text = msg.text(); + logs.push(text); + console.log(`[Browser] ${text}`); + }); + + const requests: string[] = []; + page.on('request', request => { + if (request.isNavigationRequest()) { + requests.push(request.url()); + } + }); + + await mockUserfrontApisForRepro(page, { sessionStatus: 401 }); + + const targetUrl = '/ko/signin?login_challenge=repro_challenge_12345'; + await page.goto(targetUrl); + + // WASM 앱 로딩 및 로직 실행 대기 + await page.waitForTimeout(7000); + + const currentUrl = page.url(); + const signinNavigations = requests.filter(url => url.includes('/signin')); + + // [검증 1] URL 유지 확인 + expect(currentUrl).toContain('login_challenge=repro_challenge_12345'); + + // [검증 2] 리다이렉트 루프 발생 여부 확인 (최초 진입 1회만 있어야 함) + expect(signinNavigations.length).toBeLessThanOrEqual(1); + + // [검증 3] 핵심 로직 로그 확인 (성공의 결정적 증거) + // 이전에는 여기서 Exception이 발생했으나, 이제는 아래 로그가 찍혀야 함 + const hasSuccessLog = logs.some(log => + log.includes('[Auth] OIDC auto-accept: No active session (status: 401)') + ); + + expect(hasSuccessLog).toBe(true); + + console.log('✅ 루프가 해결되었으며, 로그 검증을 통해 정상 동작을 확인했습니다.'); + }); +}); diff --git a/userfront/lib/features/auth/presentation/login_screen.dart b/userfront/lib/features/auth/presentation/login_screen.dart index 43f51370..5f23e16f 100644 --- a/userfront/lib/features/auth/presentation/login_screen.dart +++ b/userfront/lib/features/auth/presentation/login_screen.dart @@ -161,7 +161,12 @@ class _LoginScreenState extends ConsumerState final provider = pendingProvider ?? AuthTokenStore.getProvider() ?? 'ory'; try { - await AuthProxyService.checkCookieSession(); + final status = await AuthProxyService.getSessionStatus(useCookie: true); + if (status != 200) { + debugPrint("[Auth] Cookie session check: No active session (status: $status)"); + return; + } + if (!shouldPromoteCookieSession( currentToken: AuthTokenStore.getToken(), loginChallenge: loginChallenge, @@ -242,11 +247,16 @@ class _LoginScreenState extends ConsumerState } try { - await AuthProxyService.checkCookieSession(); - AuthTokenStore.setCookieMode( - provider: AuthTokenStore.getProvider() ?? 'ory', - ); - await _acceptOidcLoginAndRedirect(); + // 401 응답은 세션이 없는 정상적인 상태이므로 예외로 처리하지 않고 우아하게 중단합니다. + final status = await AuthProxyService.getSessionStatus(useCookie: true); + if (status == 200) { + AuthTokenStore.setCookieMode( + provider: AuthTokenStore.getProvider() ?? 'ory', + ); + await _acceptOidcLoginAndRedirect(); + } else { + debugPrint("[Auth] OIDC auto-accept: No active session (status: $status)"); + } } catch (e) { debugPrint("[Auth] OIDC auto-accept cookie check failed: $e"); }