diff --git a/로그인과정.md b/로그인과정.md new file mode 100644 index 0000000..56eabf8 --- /dev/null +++ b/로그인과정.md @@ -0,0 +1,552 @@ +# localhost:5000 로그인 화면 호출 흐름 + +이 문서는 브라우저에서 `http://localhost:5000/`로 접근했을 때 UserFront 로그인 화면이 뜨기까지의 순차 흐름을 정리합니다. + +작성 기준: + +- 로컬 URL: `http://localhost:5000/` +- gateway: `baron_gateway` +- UserFront upstream: `baron_userfront:5000` +- 로그인 화면 구현 파일: `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` + +## 1. 결론 + +로그인 화면의 대표 URL은 다음입니다. + +- 한국어: `http://localhost:5000/ko/signin` +- 영어: `http://localhost:5000/en/signin` + +로그인 화면의 실제 소스 파일 절대경로는 다음입니다. + +```text +/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart +``` + +`http://localhost:5000/`로 접근하면 gateway가 UserFront 정적 앱을 내려주고, Flutter 앱이 기동된 뒤 `GoRouter`가 locale과 세션 상태를 판단하여 미로그인 사용자를 `/{locale}/signin`으로 보냅니다. + +## 2. 전체 순차 흐름 + +### 2.1 브라우저 최초 요청 + +1. 브라우저가 다음 URL을 요청합니다. + +```text +GET http://localhost:5000/ +``` + +2. 요청은 Docker host port `5000`으로 들어오고 `baron_gateway` Nginx가 받습니다. + +관련 파일: + +```text +/mnt/e/h_workspace/baron-sso/gateway/nginx.conf +``` + +관련 설정: + +```nginx +server { + listen 5000; + + set $backend_upstream http://baron_backend:3000; + set $userfront_upstream http://baron_userfront:5000; + set $oathkeeper_upstream http://oathkeeper:4455; + + location / { + proxy_pass $userfront_upstream; + } +} +``` + +3. `/`는 `location /`에 매칭되고 `baron_userfront:5000`으로 proxy됩니다. + +```text +browser -> localhost:5000 -> baron_gateway -> baron_userfront:5000 +``` + +### 2.2 UserFront 정적 bootstrap 응답 + +4. UserFront가 Flutter Web entry HTML을 반환합니다. + +관련 파일: + +```text +/mnt/e/h_workspace/baron-sso/userfront/web/index.html +``` + +5. `index.html`은 처음에 bootstrap shell을 보여줍니다. 이 시점에 보이는 것은 실제 Flutter 로그인 화면이 아니라 로딩용 HTML입니다. + +주요 DOM: + +```html +
+

Baron SW Portal

+ +

Loading sign-in

+ +
+``` + +6. `index.html`은 로컬 개발 환경에서 기존 service worker와 Flutter cache를 제거한 뒤 `flutter_bootstrap.js`를 로드합니다. + +사용 함수/동작: + +- `loadFlutter()` +- `navigator.serviceWorker.getRegistrations()` +- `registration.unregister()` +- `caches.keys()` +- `caches.delete()` + +이후 브라우저는 일반적으로 다음 정적 리소스를 가져옵니다. + +```text +GET /flutter_bootstrap.js +GET /manifest.json +GET /main.dart.mjs +GET /canvaskit/skwasm.js +GET /canvaskit/skwasm.wasm +GET /favicon.ico +``` + +### 2.3 Flutter 앱 시작 + +7. Flutter runtime이 `main.dart`의 `main()`을 실행합니다. + +관련 파일: + +```text +/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart +``` + +주요 함수: + +- `main()` +- `WidgetsFlutterBinding.ensureInitialized()` +- `usePathUrlStrategy()` +- `EasyLocalization.ensureInitialized()` +- `LocaleRegistry.primeWithDefaults()` +- `LoggerService.init()` +- `runApp(...)` + +8. `runApp()`은 다음 wrapper를 구성합니다. + +```text +EasyLocalization + -> ProviderScope + -> BaronSSOApp +``` + +9. `BaronSSOApp.build()`가 `MaterialApp.router`를 생성하고 `_router`를 연결합니다. + +주요 함수/객체: + +- `BaronSSOApp` +- `_BaronSSOAppState.build()` +- `MaterialApp.router(...)` +- `routerConfig: _router` + +## 3. GoRouter 라우팅 흐름 + +라우터는 `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart`에 정의되어 있습니다. + +주요 객체: + +```dart +final _router = GoRouter( + initialLocation: '/', + refreshListenable: AuthNotifier.instance, + routes: [...], + redirect: (context, state) { ... }, +); +``` + +### 3.1 locale 결정 + +`http://localhost:5000/`에는 locale path segment가 없습니다. 따라서 앱은 선호 locale을 계산합니다. + +사용 함수: + +- `extractLocaleFromPath(Uri uri)` +- `resolvePreferredLocaleCode()` +- `normalizeLocaleCode(String? code)` +- `buildLocalizedPath(String localeCode, Uri uri)` +- `buildLocalizedHomePath(Uri uri, {String? preferredLocaleCode})` + +관련 파일: + +```text +/mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart +``` + +locale 결정 기준: + +1. 저장된 locale이 있으면 사용 +2. 없으면 브라우저/디바이스 locale 사용 +3. 지원하지 않는 값이면 fallback locale 사용 + +따라서 한국어 환경에서는 보통 `ko`, 영어 환경에서는 `en`이 됩니다. + +### 3.2 세션 상태 확인 + +라우터와 앱 초기화 코드는 로컬 토큰 또는 쿠키 기반 세션 여부를 봅니다. + +사용 함수: + +- `_hasActiveLocalSession()` +- `AuthTokenStore.getToken()` +- `AuthTokenStore.usesCookie()` +- `_shouldRunStartupSessionRecovery(Uri uri)` +- `_silentSessionRecovery()` +- `AuthProxyService.getMe()` +- `AuthProxyService.getSessionStatus()` + +관련 파일: + +```text +/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart +/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart +/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart +``` + +세션 확인 시 호출될 수 있는 backend API: + +```text +GET /api/v1/user/me +``` + +gateway 기준 흐름: + +```text +browser -> /api/v1/user/me -> baron_gateway location /api -> baron_backend:3000 +``` + +### 3.3 미로그인 상태에서 최종 로그인 route로 이동 + +미로그인 상태라면 최종적으로 다음 경로로 이동합니다. + +```text +/{locale}/signin +``` + +예: + +```text +http://localhost:5000/ko/signin +http://localhost:5000/en/signin +``` + +관련 redirect 함수: + +- root route redirect: `buildLocalizedHomePath(...)` +- global router redirect: `redirect: (context, state) { ... }` +- private route guard: `_redirectPrivateLocaleRoute(...)` +- locale entry fallback: `LocaleEntryRedirectScreen._redirect()` +- signin path builder: `buildSigninRedirectPath(...)` + +중요 분기: + +- locale이 없으면 locale이 붙은 path로 보정합니다. +- public auth path는 그대로 통과합니다. +- 로그인하지 않았고 private path 또는 locale root에 있으면 `/{locale}/signin`으로 보냅니다. +- 로그인되어 있고 locale root에 있으면 `/{locale}/dashboard`로 보냅니다. + +## 4. 로그인 화면 route와 화면 생성 + +로그인 화면 route는 `main.dart`의 `/:locale/signin`입니다. + +브라우저 URL: + +```text +http://localhost:5000/ko/signin +``` + +라우터 path: + +```text +/:locale/signin +``` + +관련 코드 구조: + +```dart +GoRoute( + path: 'signin', + builder: (context, state) { + final loginChallenge = state.uri.queryParameters['login_challenge']; + final redirectUrl = + state.uri.queryParameters['redirect_uri'] ?? + state.uri.queryParameters['redirect_url']; + return ScopedTheme( + controller: ThemeController.auth, + child: LoginScreen( + key: state.pageKey, + loginChallenge: loginChallenge, + redirectUrl: redirectUrl, + ), + ); + }, +) +``` + +생성되는 widget: + +```text +ScopedTheme + -> LoginScreen +``` + +로그인 화면 구현 파일: + +```text +/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart +``` + +주요 class/function: + +- `LoginScreen` +- `_LoginScreenState` +- `_LoginScreenState.initState()` +- `_LoginScreenState.build()` +- `_handlePasswordLogin()` +- `_handleLinkLogin()` +- `_startEnchantedFlow()` +- `_pollForSession()` +- `_startQrFlow()` +- `_startQrPolling()` +- `_tryCookieSession()` +- `_attemptOidcAutoAccept()` +- `_acceptOidcLoginAndRedirect()` +- `_onLoginSuccess()` + +## 5. LoginScreen 초기화 흐름 + +`LoginScreen`이 생성되면 `_LoginScreenState.initState()`가 실행됩니다. + +초기화 순서: + +1. `TabController(length: 3)` 생성 +2. password/link/QR 입력 controller 준비 +3. `drySend` query parameter 확인 +4. `redirect_uri` 또는 `redirect_url` query parameter 확인 +5. `login_challenge` 확인 +6. magic link, short code, verification token이 있는지 확인 +7. QR 로그인 필요 안내 notice 확인 +8. OIDC login challenge가 있으면 자동 accept 시도 +9. tenant-info 조회로 login ID label 커스터마이징 +10. login challenge가 없으면 쿠키 세션 확인 + +초기 진입이 단순 `http://localhost:5000/`인 경우 보통 query parameter가 없으므로 다음 정도가 수행됩니다. + +```text +LoginScreen.initState() + -> _resolveLoginChallenge() + -> _attemptOidcAutoAccept() // login_challenge 없으면 바로 return + -> AuthProxyService.getTenantInfo() + -> _tryCookieSession() +``` + +초기 화면 렌더링에 영향을 줄 수 있는 backend API: + +```text +GET /api/v1/auth/tenant-info +GET /api/v1/user/me +``` + +## 6. 로그인 화면 UI 구성 + +`LoginScreen.build()`는 탭 기반 로그인 UI를 구성합니다. + +탭: + +```text +1. 비밀번호 +2. 로그인 링크 +3. QR 코드 +``` + +관련 UI 코드: + +- `TabBar` +- `TabBarView` +- password tab +- link tab +- QR tab + +### 6.1 비밀번호 탭 + +UI 요소: + +- 로그인 ID 입력 +- 비밀번호 입력 +- 로그인 버튼 + +주요 key: + +```text +password_login_id_input +password_login_password_input +password_login_submit_button +``` + +사용 함수: + +- `_handlePasswordLogin()` +- `AuthProxyService.loginWithPassword(...)` +- `_onLoginSuccess(...)` + +호출 API: + +```text +POST /api/v1/auth/password/login +``` + +### 6.2 로그인 링크 탭 + +UI 요소: + +- 로그인 ID 입력 +- 로그인 링크 전송 버튼 +- short code 입력 UI +- 재전송 버튼 + +사용 함수: + +- `_handleLinkLogin()` +- `_startEnchantedFlow(...)` +- `_pollForSession(...)` +- `_verifyShortCode(...)` +- `AuthProxyService.initEnchantedLink(...)` +- `AuthProxyService.pollEnchantedLink(...)` +- `AuthProxyService.verifyLoginShortCode(...)` + +호출 API: + +```text +POST /api/v1/auth/enchanted-link/init +POST /api/v1/auth/enchanted-link/poll +POST /api/v1/auth/login/code/verify-short +``` + +### 6.3 QR 코드 탭 + +QR 탭은 선택될 때 QR flow를 시작합니다. + +사용 함수: + +- `_handleTabSelection()` +- `_startQrFlow()` +- `_startQrPolling()` +- `_startCountdown()` +- `AuthProxyService.initQrLogin()` +- `AuthProxyService.pollQrStatus(...)` + +호출 API: + +```text +POST /api/v1/auth/qr/init +POST /api/v1/auth/qr/poll +``` + +## 7. OIDC login_challenge가 있는 경우 + +외부 RP 또는 Hydra 흐름에서 다음과 같은 URL로 들어올 수 있습니다. + +```text +http://localhost:5000/ko/signin?login_challenge=... +``` + +이 경우 `LoginScreen`은 `login_challenge`를 보관하고 다음을 시도합니다. + +1. `_attemptOidcAutoAccept()` +2. 로컬 token 또는 cookie session이 있으면 backend에 login accept 요청 +3. backend가 반환한 `redirectTo`로 브라우저 이동 + +사용 함수: + +- `_resolveLoginChallenge(...)` +- `_attemptOidcAutoAccept()` +- `_acceptOidcLoginAndRedirect(...)` +- `_redirectToOidcTarget(...)` +- `AuthProxyService.acceptOidcLogin(...)` + +호출 API: + +```text +POST /api/v1/auth/oidc/login/accept +``` + +로그인 화면에서 사용자가 비밀번호 로그인을 완료한 뒤에도 `_onLoginSuccess()`가 `login_challenge`를 감지하면 같은 accept 흐름을 수행합니다. + +## 8. 페이지/route 목록 + +UserFront auth 관련 route는 locale 아래에 붙습니다. + +| 브라우저 절대 URL 예시 | GoRouter path | 생성 화면 | 구현 파일 | +| --- | --- | --- | --- | +| `http://localhost:5000/ko/signin` | `/:locale/signin` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` | +| `http://localhost:5000/ko/login` | `/:locale/login` | `LoginScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` | +| `http://localhost:5000/ko/signup` | `/:locale/signup` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` | +| `http://localhost:5000/ko/registration` | `/:locale/registration` | `SignupScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/signup_screen.dart` | +| `http://localhost:5000/ko/consent?consent_challenge=...` | `/:locale/consent` | `ConsentScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/consent_screen.dart` | +| `http://localhost:5000/ko/forgot-password` | `/:locale/forgot-password` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` | +| `http://localhost:5000/ko/recovery` | `/:locale/recovery` | `ForgotPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/forgot_password_screen.dart` | +| `http://localhost:5000/ko/reset-password` | `/:locale/reset-password` | `ResetPasswordScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/reset_password_screen.dart` | +| `http://localhost:5000/ko/error` | `/:locale/error` | `ErrorScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/error_screen.dart` | +| `http://localhost:5000/ko/dashboard` | `/:locale/dashboard` | `DashboardScreen` | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/dashboard/presentation/dashboard_screen.dart` | + +## 9. 가장 중요한 파일 요약 + +| 목적 | 파일 절대경로 | +| --- | --- | +| gateway proxy 설정 | `/mnt/e/h_workspace/baron-sso/gateway/nginx.conf` | +| Flutter Web HTML bootstrap | `/mnt/e/h_workspace/baron-sso/userfront/web/index.html` | +| 앱 시작점과 GoRouter 정의 | `/mnt/e/h_workspace/baron-sso/userfront/lib/main.dart` | +| 로그인 화면 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/presentation/login_screen.dart` | +| 로그인/API proxy client | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_proxy_service.dart` | +| token/cookie session store adapter | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/services/auth_token_store.dart` | +| locale path helper | `/mnt/e/h_workspace/baron-sso/userfront/lib/core/i18n/locale_utils.dart` | +| public auth path 판정 | `/mnt/e/h_workspace/baron-sso/userfront/lib/features/auth/domain/login_link_route_policy.dart` | + +## 10. 짧은 호출 흐름 다이어그램 + +```mermaid +sequenceDiagram + participant B as Browser + participant G as baron_gateway
localhost:5000 + participant UF as baron_userfront:5000 + participant FE as Flutter UserFront + participant BE as baron_backend:3000 + + B->>G: GET / + G->>UF: proxy GET / + UF-->>G: index.html + G-->>B: index.html + B->>G: GET /flutter_bootstrap.js + G->>UF: proxy static asset + B->>G: GET /main.dart.mjs 등 + G->>UF: proxy static assets + FE->>FE: main() + FE->>FE: MaterialApp.router(_router) + FE->>FE: locale 결정 + FE->>FE: local token/cookie 상태 확인 + alt 미로그인 + FE->>FE: route -> /{locale}/signin + FE->>BE: GET /api/v1/auth/tenant-info + FE->>BE: GET /api/v1/user/me (cookie/session check 가능) + FE->>B: LoginScreen 렌더링 + else 로그인됨 + FE->>FE: route -> /{locale}/dashboard + end +``` + +## 11. 디버깅 포인트 + +로그인 화면이 뜨지 않을 때 먼저 확인할 것: + +1. `baron_gateway`가 떠 있는지 +2. `baron_userfront`가 healthy인지 +3. gateway 로그에 `baron_userfront could not be resolved` 또는 `connect() failed`가 있는지 +4. 브라우저 Network 탭에서 `/flutter_bootstrap.js`, `/main.dart.mjs`가 200인지 +5. Flutter route가 `/ko/signin` 또는 `/en/signin`으로 이동하는지 +6. `/api/v1/auth/tenant-info`, `/api/v1/user/me` 실패가 화면 렌더링을 막는지 +