diff --git a/.gitignore b/.gitignore index a4d2df8d..2e4a11ea 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,8 @@ .DS_Store .idea/ .vscode/ +.codex +.codex/ *.swp *.log diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index b5a06c63..73a1298f 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -3390,7 +3390,9 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { } if approvedSessionID == "" { if token := h.getBearerToken(c); token != "" { - approvedSessionID = extractSessionIDFromJWT(token) + if resolved, err := h.getKratosSessionID(token); err == nil { + approvedSessionID = resolved + } } } if approvedSessionID == "" { @@ -3401,7 +3403,6 @@ func (h *AuthHandler) AcceptOidcLoginRequest(c *fiber.Ctx) error { } } if approvedSessionID != "" { - c.Locals("session_id", approvedSessionID) c.Locals("approved_session_id", approvedSessionID) } if h.KratosAdmin != nil { diff --git a/docs/descope-federated-poc.md b/docs/descope-federated-poc.md deleted file mode 100644 index 4e4f1239..00000000 --- a/docs/descope-federated-poc.md +++ /dev/null @@ -1,82 +0,0 @@ -# Descope Federated Apps 연동 PoC (Hydra 1st party 보조) - -목표: Hydra를 1st party OIDC 엔진으로 유지하면서 Descope Federated Apps를 통해 외부 IDP(SAML/OIDC)를 빠르게 붙여 BYOID/소셜을 지원한다. Kratos/DB가 SoT가 되고, Hydra 토큰의 subject는 Kratos identity.id로 맞춘다. - -## 플로우(로그인) -```mermaid -sequenceDiagram - participant RP as RP App - participant HY as Hydra - participant UI as Baron Login UI - participant DS as Descope Federated App - participant IDP as Ext. IDP (SAML/OIDC) - participant KR as Kratos - RP->>HY: /oauth2/auth (login_challenge) - HY->>UI: redirect to login UI - UI-->>UI: 사용자 선택: "Descope Federated App" - UI->>DS: redirect to Descope Federated App (idp=azuread 등) - DS->>IDP: AuthN (SAML/OIDC) - IDP-->>DS: Assertion/Token - DS-->>UI: redirect back with session/authorization data - UI->>KR: upsert identity (traits from DS) - UI->>KR: CreateSession (kratos admin) - UI->>HY: AcceptLogin(subject=kratos identity.id, amr=descope-federated-{idp}) - HY-->>RP: ID/Access Token 발급 (subject=kratos identity.id) -``` - -## 환경 변수/구성 요소 -- Hydra: `HYDRA_ADMIN_URL`, `HYDRA_PUBLIC_URL`, 등록된 RP 클라이언트(redirect URI 포함). -- Kratos: `KRATOS_ADMIN_URL`, `KRATOS_PUBLIC_URL`, identity schema에 federated trait 필드 포함. -- Descope: `DESCOPE_PROJECT_ID`, `DESCOPE_MANAGEMENT_KEY`, `DESCOPE_FEDERATED_APP_ID`, (옵션) `DESCOPE_TENANT`, `DESCOPE_BASE_URL`(self-host 시). -- Baron UI/백엔드: Federated 버튼 노출 플래그, callback URL (`/auth/federated/descope/callback` 등), Hydra login_challenge 전달 경로. - -## 데이터 매핑/저장 -- Kratos identity traits 예: `email`, `email_verified`, `name`, `phone_number`, `department`, `grade`, `federated_identities` 배열. -- federated_identities 제안 스키마 - - `provider` (예: `descope-federated-azuread`) - - `idp_sub` - - `idp_email` - - `idp_email_verified` - - `raw_claims` (JSON, 최소 보관) - - `last_login_at` -- 최초 로그인: email match로 기존 identity 찾기 → 없으면 생성 → federated slot 추가. -- 재로그인: provider+sub 매칭 후 Kratos identity.id 사용. - -## 구현 단계 (PoC) -1) **Hydra 클라이언트 준비** - - RP별 redirect URI 등록, `skip_consent` 옵션은 false 권장(동의 화면 제공 시). - - `ory-net` 네트워크에서 Hydra Admin 접근 허용. -2) **Descope Federated App 설정** - - Federated App 생성 후 외부 IDP(SAML/OIDC) 연결. - - Callback을 Baron Login UI로 설정(`/auth/federated/descope/callback?login_challenge=...`). - - 전달 클레임: `sub`, `email`, `email_verified`, `name`, `groups`(option), `phone_number`. -3) **Login UI/Backend 연결** - - 옵션 버튼 “Sign in with (via Descope)” 추가. - - Start 엔드포인트: Hydra login_challenge를 받고 Descope Federated App redirect URL 생성 후 302. - - Callback 핸들러: - - Descope session/token 검증 (관리 키 또는 JWKS). - - Kratos identity upsert (traits + federated slot). - - Kratos `CreateSession` 호출 → 세션 쿠키 설정. - - Hydra `AcceptLogin` 호출(subject=kratos identity.id, amr=`federated:descope-{idp}`) → Hydra redirect. -4) **로그/감사** - - Hydra login_challenge, provider, sub, email, amr 기록. - - 실패 시 Hydra `RejectLogin`로 일관된 에러 제공. - -## 보안/운영 체크 -- `email_verified` 필수 검증, 미확인 이메일은 거절 또는 별도 플로우. -- 토큰/세션 검증 시 JWKS 캐시 및 만료 확인. -- Rate limit: Federated callback, Hydra login_challenge 재사용 방지. -- PII 최소 저장: raw_claims는 단기 TTL 또는 축약 저장. -- 장애 시 폴백: `IDP_PROVIDER=ory,descope` 설정으로 Descope 기본 로컬 로그인 경로 유지. - -## 테스트 시나리오 (PoC) -- 성공: Federated 버튼 → 외부 IDP 로그인 → Hydra 토큰 발급, subject=kratos identity.id 확인. -- 이메일 검증 실패: email_verified=false인 경우 거절 메시지. -- 재로그인: 기존 federated_identities 매칭 후 동일 subject 유지. -- 오류: 잘못된 login_challenge, 만료된 Descope 토큰, Hydra RejectLogin 동작 확인. - -## 후속 구현(코드) -- Ory IDP 어댑터에 Kratos Admin/Hydra Admin 연동 구현 (`InitiatePasswordReset`, `VerifyPasswordResetToken`, `UpdateUserPassword` 포함). -- AuthHandler에서 Descope 종속 로직을 IDP 추상화 기반으로 재구성(비밀번호 재설정/가입/로그인 모두). -- Login UI에 Federated 버튼 및 상태 처리 추가. -- CI에서 ory-stack 기동 + federated mock IDP로 통합 테스트 추가. diff --git a/docs/descope_inbound_apps.md b/docs/descope_inbound_apps.md deleted file mode 100644 index eae700ba..00000000 --- a/docs/descope_inbound_apps.md +++ /dev/null @@ -1,135 +0,0 @@ -**Descope Inbound App은 “OIDC 클라이언트 + 사용자 동의(Consent) + 토큰/세션 정책”을 한 화면에 묶어 제공하는 콘솔**입니다. - ---- - -## 1️⃣ Inbound App 기본 정보 (OIDC Client 메타데이터) - -**역할: OAuth/OIDC Client 정의** - -- **Inbound App Name / ID** - - OIDC `client_id`에 대응 - -- **Description** -- **Status (Verified / Unverified)** - - 사용자에게 신뢰된 앱인지 표시용 - -👉 Hydra 기준으로 보면 `hydra clients create`의 **client 메타 정보 영역** - ---- - -## 2️⃣ Scopes 관리 (권한 정의) - -**역할: “이 앱이 무엇을 요구할 수 있는가” 정의** - -### Permission Scopes - -- `full_access` -- `profile` -- `email` -- 각 scope별: - - 설명 - - Role 연계 여부 - - Mandatory 여부 - -### User Information Scopes - -- 사용자 claims에 포함될 정보 정의 -- “토큰에 항상 authorization 정보 포함” 옵션 - -👉 OIDC의 `scope` + `claims` 설계를 **UI로 추상화** - ---- - -## 3️⃣ Consents (사용자 동의) 탭 - -**역할: Descope 인바운드 앱의 핵심** - -- 사용자가 로그인 중 보게 되는 화면 -- “이 앱이 아래 권한을 요청합니다” -- Scope별 동의/거부 -- Mandatory scope는 자동 포함 - -👉 이게 **Hydra의 `consent_challenge`를 처리하는 UI**에 해당 -👉 김용연님이 **5174에 구현하라고 들은 바로 그 기능** - ---- - -## 4️⃣ Connection Information (OIDC Endpoint 묶음) - -**역할: 외부 앱이 실제로 연동할 정보** - -- **Flow Hosting URL** - - Descope가 제공하는 로그인 + consent orchestration URL - -- **Approved Redirect URIs** - - OAuth redirect whitelist - -- **Client ID / Client Secret** -- **Discovery URL** - - `/.well-known/openid-configuration` - -- **Issuer** -- **Authorization URL** -- **Token URL** -- **Audience Whitelist** -- **Default Audience 설정** - -👉 이 영역은 **OIDC 표준 설정을 전부 노출** -👉 Hydra로 치면: - -- discovery -- issuer -- `/oauth2/auth` -- `/oauth2/token` - ---- - -## 5️⃣ Session Management (토큰/세션 정책) - -**역할: 보안 정책 제어** - -### Token Format - -- User JWT 템플릿 -- Access Key JWT 템플릿 - -### Token Expiration - -- Refresh Token Timeout (예: 520주) -- Session Token Timeout (분 단위) -- Access Token Timeout - -👉 Hydra + Kratos 설정을 **앱 단위로 override**하는 개념 - ---- - -## 6️⃣ Descope Inbound App이 “한 번에 제공하는 것” 요약 - -한 문장으로 정리하면: - -> **Descope Inbound App = -> OIDC Client 관리 + Scope 정의 + Consent UI + Token/Session 정책 + Login Flow Hosting** - ---- - -## 7️⃣ 김용연님 Baron SSO(5174)와의 1:1 대응표 - -| Descope Inbound App | Baron SSO(5174) | -| ---------------------- | ------------------------------- | -| Inbound App Details | Client 관리 화면 | -| Scopes | Client Scope 설정 | -| **Consents** | **/consent 페이지 (구현 대상)** | -| Connection Information | Hydra Client 설정 | -| Session Management | 토큰 정책 설정 | - -👉 그래서 결론적으로, - -- **지금 5174 `/clients` 화면은 Descope의 “Settings 탭 일부”** -- **Consents 탭이 빠져 있어서 아직 Descope의 절반만 구현된 상태** - ---- - -## 최종 정리 한 줄 - -> **Descope Inbound App은 “OIDC Client + 사용자 동의 + 보안 정책”을 묶은 올인원 인바운드 애플리케이션 콘솔이고, -> 5174는 그걸 Hydra 기반으로 우리가 직접 재구현하는 중이다.** diff --git a/docs/initial_PRD.md b/docs/initial_PRD.md index 15fedbad..a0549720 100644 --- a/docs/initial_PRD.md +++ b/docs/initial_PRD.md @@ -1,18 +1,20 @@ -# Baron SSO - Product Requirements Document (PRD) +# Product Requirements Document (PRD) ## 1. 개요 (Overview) -**Baron SSO**는 사용자 중심의 인증 허브이자 서비스 런처입니다. Descope를 IDP(Identity Provider)로 사용하여 강력한 보안(MFA)을 제공하면서도, 최종 사용자에게는 **Baron SSO**라는 브랜드 경험을 일관되게 제공하는 것을 목표로 합니다. +**Baron SSO**는 사용자 중심의 인증 허브이자 서비스 런처입니다. Ory-Stack 중 Kratos를 IDP(Identity Provider)로 사용하여 강력한 보안(MFA)을 제공하면서도, 최종 사용자에게는 **Baron SSO**라는 브랜드 경험을 일관되게 제공하는 것을 목표로 합니다. ## 2. 목표 (Goals) - **Private IDP Hub**: 사용자가 자신의 계정 및 로그인 세션을 한곳에서 관리. -- **Seamless Auth**: Descope의 Enchanted Link를 활용하여 비밀번호 없는 간편 로그인 제공. -- **White-Labeling**: Descope의 UI를 노출하지 않고 자체 Flutter UI로 인증 흐름 완결. +- **Seamless Auth**: 비밀번호 없는 간편 로그인 제공. +- **White-Labeling**: 자체 Flutter UI로 인증 흐름 완결. - **Audit & Security**: 모든 중요 인증 이벤트 및 접근 기록을 자체 Backend(Go Fiber)를 통해 ClickHouse에 Audit Log로 저장. - **Unified Launcher**: 인증 후 접근 가능한 서비스들을 한 화면에서 제공. ## 3. 기술 스택 (Tech Stack) -- **Frontend**: Flutter (Web PoC 우선, 추후 iOS/Android) -- **Backend (IDP)**: Descope (Auth Logic, User Management) +- **UserFront**: Flutter (Web PoC 우선, 추후 iOS/Android) +- **AdminFront**: Vite, React, Shadcn/ui, biome, playwright +- **DevFront**: Vite, React, Shadcn/ui, biome, playwright +- **Ory (IDP)**: Ory (Oathkeeper, Kratos, Hydra, Keto, Posgres) - **Backend (Audit/API)**: Go (Fiber Framework) - **Database**: Postgres (Meta), ClickHouse (Audit Logs) - **Protocol**: OIDC/SAML (Service Integration), REST (Audit) @@ -21,23 +23,22 @@ ### 4.1 로그인 및 인증 (Authentication) - **로그인 방식 1 (Primary)**: 이메일 + 비밀번호 (Email/Password). -- **로그인 방식 2 (Alternative)**: 전화번호 입력 -> Enchanted Link (SMS via Ncloud) -> 링크 클릭 -> 앱 로그인 완료 (Polling). +- **로그인 방식 2 (Alternative)**: 전화번호 입력 -> Link with Code (SMS via Ncloud) -> 링크 클릭 -> 앱 로그인 완료 (Polling). - **SMS Provider**: Ncloud (Naver Cloud Platform) 연동. - **MFA (Multi-Factor Authentication)**: 필요 시 TOTP 또는 생체 인증 추가 (Descope Flow 설정). <- PoC Scope Out -- **Descope Hiding**: Descope의 기본 화면 대신 Flutter로 구현된 커스텀 UI 사용. + ### 4.2 대시보드 (Dashboard) - **내 계정 정보**: 프로필 확인/수정. - **활성 세션 리스트 (Active Sessions)**: 현재 로그인되어 있는 기기/브라우저 목록 확인 및 원격 로그아웃. -### 4.3 통합 런처 (Unified Launcher) +### 4.3 통합 런처 (UserFront) - 로그인 승인 후 진입하는 메인 화면. - 접근 권한이 있는 하위 서비스 아이콘 나열 및 SSO 연결. -### 4.4 감사 로그 (Audit Backend) -### 4.5 관리자 모드 (Admin Mode) -- Descope API를 활용한 사용자 관리 (생성, 삭제, 상태 변경). -- Descope 설정 및 흐름 관리 기능 노출 (필요 시). +### 4.4 관리자 사이트 (AdminFront) + +### 4.5 개발자 사이트 (DevFront) ## 5. 사용자 시나리오 (User Flow) 1. 사용자가 Baron SSO 웹앱에 접속한다. diff --git a/docs/kratos-integration-report.md b/docs/kratos-integration-report.md deleted file mode 100644 index 2fd69fb7..00000000 --- a/docs/kratos-integration-report.md +++ /dev/null @@ -1,34 +0,0 @@ -# Ory Kratos 인증 엔진 전환 작업 보고서 - -## 1. 개요 -기존 Descope SaaS 기반 인증 시스템을 자가 호스팅(Self-hosted) IDP인 **Ory Kratos**로 전환하고, 이를 백엔드(Go Fiber)와 연동하는 작업을 수행하였습니다. - -## 2. 주요 작업 내용 - -### 2.1 인프라 및 SDK 설정 -* **SDK 설치**: Ory Kratos Go SDK (`github.com/ory/kratos-client-go`)를 백엔드 프로젝트에 추가. -* **클라이언트 초기화**: `AuthHandler` 내부에 Kratos Public API 통신을 위한 API Client 주입 및 환경 변수 연동. - -### 2.2 인증 Flow 핸들러 구현 (`auth_handler.go`) -Ory Kratos의 API-first 방식(Native Flow)에 맞춘 신규 핸들러 구현: -* **InitializeLoginFlow**: 로그인 프로세스 시작을 위한 `flow_id` 발급 API. -* **InitializeRegistrationFlow**: 회원가입 프로세스 시작을 위한 `flow_id` 발급 API. -* **LoginSubmit**: 사용자의 ID/PW를 Kratos에 제출하고 성공 시 세션 쿠키를 클라이언트에 전달. -* **RegistrationSubmit**: 커스텀 Traits(사용자 정보)와 비밀번호를 Kratos에 전달하여 계정 생성. - -### 2.3 라우팅 설정 (`main.go`) -신규 인증 엔진을 위한 전용 엔드포인트 그룹 등록: -* `GET /api/v1/auth/ory/login/initialize` -* `POST /api/v1/auth/ory/login/submit` -* `GET /api/v1/auth/ory/registration/initialize` -* `POST /api/v1/auth/ory/registration/submit` - -### 2.4 보안 및 감사 (Security & Audit) -* **세션 관리**: Kratos에서 발급한 `Set-Cookie` 헤더를 추출하여 클라이언트에 투명하게 전달(Pass-through). -* **감사 로그**: 로그인 시도 및 성공 시 시각, IP, 대상 아이디 등을 ClickHouse 감사 로그 시스템에 기록. -* **타입 오류 해결**: Kratos SDK의 구조체 타입 미스매치 이슈 해결(`result.Session` nil 비교 로직 수정). - -## 3. 향후 과제 (Next Steps) -1. **UI 연동**: `userfront` (Flutter)의 API 엔드포인트를 기존 Descope에서 신규 Ory 경로로 전환. -2. **계정 복구**: 비밀번호 찾기(Recovery) 및 이메일 확인(Verification) Flow 추가 연동. -3. **관리자 기능**: `adminfront`에서 Kratos Identities를 직접 조회/삭제하는 관리 API 연결. diff --git a/docs/kratos-todo-list.md b/docs/kratos-todo-list.md deleted file mode 100644 index 8e331c4f..00000000 --- a/docs/kratos-todo-list.md +++ /dev/null @@ -1,27 +0,0 @@ -# Kratos 기반 SSO 추가 기능 구현 로드맵 - -Ory Kratos의 표준 기능을 바탕으로 우리 프로젝트에 추가해야 할 핵심 기능 목록입니다. - -## 1. 인증 수단 고도화 -- [ ] **소셜 로그인 연동**: Google, GitHub, Apple 등 주요 OIDC 제공자 연결. -- [ ] **Passkeys (WebAuthn)**: 생체 인증을 통한 Passwordless 로그인 구현. -- [ ] **MFA (Multi-Factor Authentication)**: TOTP(Authenticator 앱), Lookup Secret(복구 코드) 지원. - -## 2. 사용자 셀프 서비스 (Self-Service) -- [ ] **계정 복구 Flow**: 비밀번호 분실 시 이메일/SMS 링크를 통한 재설정 기능. -- [ ] **계정 확인 (Verification)**: 가입 시 이메일/전화번호 점유 인증 절차. -- [ ] **프로필 및 설정 화면**: 사용자가 직접 자신의 정보(Traits)와 비밀번호를 수정하는 화면. - -## 3. 세션 보안 관리 -- [ ] **기기별 세션 관리**: 현재 로그인된 모든 브라우저/기기 목록 조회 및 특정 세션 강제 종료 기능. -- [ ] **보안 로그 제공**: 사용자 본인의 최근 로그인 기록 및 보안 이벤트 확인 기능. - -## 4. 관리자 기능 (Admin Operations) -- [ ] **커스텀 아이덴티티 스키마**: 테넌트 요구사항에 맞춘 사용자 필드(부서, 직번 등) 동적 정의. -- [ ] **사용자 일괄 마이그레이션**: 외부 데이터 대량 Import/Export API 및 도구. -- [ ] **계정 상태 강제 제어**: 관리자에 의한 계정 잠금(Ban) 및 활성화 처리. - -## 5. 시스템 연동 및 브랜딩 -- [ ] **메시지 템플릿 관리**: 이메일/SMS 발송 템플릿의 커스텀 HTML 에디터 및 미리보기. -- [ ] **Webhook 이벤트 연동**: 가입/로그인 등 주요 이벤트 발생 시 외부 시스템으로 실시간 데이터 전송. -- [ ] **멀티테넌시 브랜딩**: 접속 도메인이나 테넌트에 따른 로그인 화면 로고/컬러 동적 적용. diff --git a/docs/hydra-rp-consent-try.md b/docs/trouble-shooting/hydra-rp-consent-try.md similarity index 100% rename from docs/hydra-rp-consent-try.md rename to docs/trouble-shooting/hydra-rp-consent-try.md diff --git a/docs/issue-146-remote-login.md b/docs/trouble-shooting/issue-146-remote-login.md similarity index 100% rename from docs/issue-146-remote-login.md rename to docs/trouble-shooting/issue-146-remote-login.md diff --git a/userfront/lib/core/services/auth_proxy_service.dart b/userfront/lib/core/services/auth_proxy_service.dart index 2a60ea7b..92ca4a40 100644 --- a/userfront/lib/core/services/auth_proxy_service.dart +++ b/userfront/lib/core/services/auth_proxy_service.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; -import 'auth_token_store.dart'; import 'http_client.dart'; import 'web_window.dart'; @@ -266,7 +265,6 @@ class AuthProxyService { if (token != null && token.isNotEmpty) { headers['Authorization'] = 'Bearer $token'; } - final sessionId = _extractSessionIdFromJwt(token ?? AuthTokenStore.getToken() ?? ''); final client = createHttpClient(withCredentials: true); try { final response = await client.post( @@ -274,8 +272,6 @@ class AuthProxyService { headers: headers, body: jsonEncode({ 'login_challenge': loginChallenge, - if (sessionId != null && sessionId.isNotEmpty) - 'approved_session_id': sessionId, }), ); @@ -290,35 +286,6 @@ class AuthProxyService { } } - static String? _extractSessionIdFromJwt(String token) { - if (token.isEmpty) { - return null; - } - try { - final parts = token.split('.'); - if (parts.length != 3) { - return null; - } - final payload = utf8.decode(base64Url.decode(base64Url.normalize(parts[1]))); - final data = json.decode(payload) as Map; - for (final key in ['sid', 'session_id', 'sessionId', 'jti']) { - final value = data[key]; - if (value == null) { - continue; - } - if (value is String && value.isNotEmpty) { - return value; - } - final converted = value.toString(); - if (converted.isNotEmpty) { - return converted; - } - } - } catch (_) { - return null; - } - return null; - } static Future> initiatePasswordReset(String loginId, {bool? drySend}) async { final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate');