1
0
forked from baron/baron-sso

인증수단 세션 확인 추가

This commit is contained in:
Lectom C Han
2026-02-04 14:33:47 +09:00
parent bf469b1eb4
commit d3facfbe77
10 changed files with 19 additions and 326 deletions

2
.gitignore vendored
View File

@@ -4,6 +4,8 @@
.DS_Store
.idea/
.vscode/
.codex
.codex/
*.swp
*.log

View File

@@ -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 {

View File

@@ -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 <IDP> (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로 통합 테스트 추가.

View File

@@ -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 기반으로 우리가 직접 재구현하는 중이다.**

View File

@@ -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 웹앱에 접속한다.

View File

@@ -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 연결.

View File

@@ -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 이벤트 연동**: 가입/로그인 등 주요 이벤트 발생 시 외부 시스템으로 실시간 데이터 전송.
- [ ] **멀티테넌시 브랜딩**: 접속 도메인이나 테넌트에 따른 로그인 화면 로고/컬러 동적 적용.

View File

@@ -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<String, dynamic>;
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<Map<String, dynamic>> initiatePasswordReset(String loginId, {bool? drySend}) async {
final url = Uri.parse('$_baseUrl/api/v1/auth/password/reset/initiate');