# Baron SSO 로그인 방식 및 비표준 인증 흐름 정리
작성일: 2026-06-22
## 1. 목적
본 문서는 Baron SSO의 로그인 방식을 표준 방식과 비표준 방식으로 구분하고,
특히 비표준 방식에서 데이터와 인증 상태가 어떻게 이동하는지 설명하기 위해 작성한다.
- 표준 로그인 방식에 대한 이해
- 비표준 로그인 방식에서 데이터와 인증 흐름에 대한 이해 및 설명
- 사용자가 아닌 사람의 로그인 요청이 발견될 때 사용자에게 알리고, 차단 또는 비차단 요구를 받아 단계적으로 처리하는 방안 검토
## 2. 현재 로그인 방식 요약
Baron SSO는 Ory Kratos와 Ory Hydra를 기반으로 한다.
- Kratos: 사용자 인증, 세션 발급, 비밀번호 및 코드 로그인 처리
- Hydra: OIDC/OAuth2 인가 서버, RP 로그인 요청과 consent 처리
- UserFront: 사용자 로그인 UI
- Backend: UserFront와 Ory 사이의 중계 및 Baron 커스텀 로그인 흐름 처리
현재 사용자 로그인 방식은 아래처럼 5개로 볼 수 있다.
| 구분 | 방식 | 대표 엔드포인트 | 성격 |
| --- | --- | --- | --- |
| 표준 | ID/Password | `POST /api/v1/auth/password/login` | Kratos password login |
| 표준 | Login Code | `POST /api/v1/auth/login/code/verify` | Kratos code login |
| 비표준 | Enchanted Link | `POST /api/v1/auth/enchanted-link/init` -> `poll` | Baron pendingRef 기반 링크 승인 |
| 비표준 | Magic Link Verify | `POST /api/v1/auth/magic-link/verify` | Baron token 검증 및 verify-only |
| 비표준 | QR 로그인 | `POST /api/v1/auth/qr/init` -> `poll` -> `approve` | Baron 교차 기기 승인 |
참고로 `POST /api/v1/auth/sms`, `POST /api/v1/auth/verify-sms`도 존재하지만, 위의 "표준 2개 + 비표준 3개" 분류에서는 별도 보조 또는 내부 토큰 기반 흐름으로 보는 것이 적절하다.
### 2.1 운영 요청 경로와 Gateway 역할
운영 경로에서는 사용자의 요청이 곧바로 Backend, Kratos, Hydra로 들어가지 않는다. 앞단에는 Traefik 또는 외부 L7, Baron Gateway Nginx, Oathkeeper가 있다.
현재 `gateway/nginx.conf` 기준 역할은 다음과 같다.
| 경로 | Nginx 처리 | 다음 대상 |
| --- | --- | --- |
| `/api/*` | Backend API proxy | `baron_backend:3000` |
| `/auth/*` | Ory Stack proxy | `oathkeeper:4455` -> Kratos public |
| `/oidc/*` | Ory Stack proxy | `oathkeeper:4455` -> Hydra public |
| `/` | UserFront 정적/앱 proxy | `baron_userfront:5000` |
그림으로 보면 다음과 같다.
```mermaid
flowchart LR
User[사용자 브라우저]
RP[RP/업무시스템]
L7[Traefik 또는 외부 L7]
GW[Nginx Baron Gateway]
UF[UserFront]
BE[Baron Backend]
OK[Oathkeeper]
KR[Ory Kratos Public]
HY[Ory Hydra Public]
User --> RP
RP -->|OIDC /oidc/oauth2/auth| L7
User -->|Baron SSO 접속| L7
L7 --> GW
GW -->|/| UF
GW -->|/api/v1/*| BE
GW -->|/auth/*| OK
GW -->|/oidc/*| OK
OK -->|Kratos public API| KR
OK -->|Hydra public API| HY
```
따라서 로그인 흐름을 설명할 때는 다음 두 경로를 구분해야 한다.
- 브라우저/API 호출 경로: `사용자 또는 RP -> L7 -> Nginx Gateway -> Backend/Oathkeeper`
- 내부 백채널 경로: `Backend -> Kratos Admin/Public`, `Backend -> Hydra Admin`, `Kratos Courier -> Backend`
## 3. 표준 방식 2개
### 3.1 ID/Password
사용자가 로그인 ID와 비밀번호를 입력하면 UserFront가 Backend의 `POST /api/v1/auth/password/login`을 호출한다.
Backend는 Kratos Public API에서 login flow를 만들고, Kratos에 다음 정보를 제출한다.
- `identifier`: 로그인 ID
- `password`: 비밀번호
- `method`: `password`
Kratos 인증이 성공하면 세션 토큰과 Kratos 세션 쿠키가 발급된다. Baron Backend는 Kratos identity ID를 Baron의 subject로 해석하고, 사용자 상태가 로그인 가능한 상태인지 확인한다.
OIDC 로그인 중이면 `login_challenge`도 함께 전달된다. 이 경우 Backend는 Hydra의 login request를 확인한 뒤 `AcceptLoginRequest`를 호출하고, Hydra가 내려준 `redirectTo`를 UserFront에 반환한다. UserFront는 이 URL로 이동하여 RP의 OIDC 흐름을 계속 진행한다.
그림으로 보면 다음과 같다.
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant UF as UserFront
participant BE as Baron Backend
participant KR as Ory Kratos
participant HY as Ory Hydra
participant RP as RP/업무시스템
User->>RP: 업무시스템 접속
RP->>HY: OIDC 인증 요청
HY->>UF: login_challenge와 함께 로그인 화면 이동
User->>UF: ID/Password 입력
UF->>BE: POST /api/v1/auth/password/login
BE->>KR: password login flow 제출
KR-->>BE: Kratos session_token, session cookie
BE->>HY: AcceptLoginRequest(login_challenge, subject)
HY-->>BE: redirectTo 반환
BE-->>UF: redirectTo 반환
UF->>RP: OIDC callback으로 이동
```
### 3.2 Login Code
Login Code는 Kratos의 `method=code` 기반 로그인이다. 사용자는 이메일 또는 전화번호 등으로 전달받은 코드를 입력하고, Backend는 Kratos login flow에 해당 코드를 제출한다.
핵심 데이터는 다음과 같다.
- `loginId`: 코드가 발급된 사용자 식별자
- `flowID`: Kratos login flow ID
- `code`: 사용자가 제출한 인증 코드
검증 성공 시 Kratos는 세션 토큰을 발급한다. Baron은 이 세션 토큰을 기준으로 UserFront 세션을 구성하거나, OIDC 흐름이 있으면 Hydra login accept로 이어간다.
그림으로 보면 다음과 같다.
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant UF as UserFront
participant BE as Baron Backend
participant KR as Ory Kratos
participant HY as Ory Hydra
User->>UF: 로그인 ID 입력
UF->>BE: 코드 로그인 시작 요청
BE->>KR: method=code login flow 시작
KR-->>User: 이메일/SMS 등으로 인증 코드 전달
User->>UF: 인증 코드 입력
UF->>BE: POST /api/v1/auth/login/code/verify
BE->>KR: VerifyLoginCode(loginId, flowID, code)
KR-->>BE: Kratos session_token
alt OIDC login_challenge 있음
BE->>HY: AcceptLoginRequest(login_challenge, subject)
HY-->>BE: redirectTo 반환
else 일반 UserFront 로그인
BE-->>UF: sessionJwt 반환
end
```
## 4. 비표준 방식 3개
비표준 방식은 Kratos/Hydra의 표준 프로토콜만으로 끝나지 않고, Baron Backend가 `pendingRef`, Redis 상태, 폴링, 교차 기기 승인 같은 추가 상태 머신을 운영한다.
### 4.1 Enchanted Link
Enchanted Link는 사용자가 로그인 ID를 입력하면 Backend가 로그인 대기 상태를 만들고, 링크 또는 코드 기반 승인을 기다리는 방식이다.
대표 흐름은 다음과 같다.
1. UserFront가 `POST /api/v1/auth/enchanted-link/init` 호출
2. Backend가 사용자 존재 여부를 확인
3. Backend가 Redis에 `pendingRef` 기준 대기 상태 저장
4. 사용자에게 링크 또는 코드 전달
5. UserFront는 `POST /api/v1/auth/enchanted-link/poll`로 상태 폴링
6. 사용자가 링크를 열거나 코드를 검증하면 대기 상태가 승인됨
7. 폴링 중인 원래 브라우저가 `sessionJwt`를 받아 로그인 완료
주요 데이터는 다음과 같다.
| 데이터 | 의미 |
| --- | --- |
| `pendingRef` | 로그인 대기 요청의 임시 참조 키 |
| `loginId` | 로그인 대상 사용자 |
| `status` | `pending`, `approved`, `success`, `expired` 등 |
| `sessionJwt` | 승인 후 발급되는 세션 토큰 |
이 방식의 특징은 인증 요청 기기와 승인 기기가 분리될 수 있다는 점이다. 따라서 "누가 요청했는지"와 "누가 승인했는지"를 반드시 분리해서 기록해야 한다.
그림으로 보면 다음과 같다.
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant UF as 요청 브라우저
participant BE as Baron Backend
participant Redis as Redis
participant Approver as 링크/코드 승인 기기
User->>UF: 로그인 ID 입력
UF->>BE: POST /api/v1/auth/enchanted-link/init
BE->>Redis: pendingRef/status=pending 저장
BE-->>User: 로그인 링크 또는 코드 전달
loop 요청 브라우저 폴링
UF->>BE: POST /api/v1/auth/enchanted-link/poll
BE->>Redis: pendingRef 상태 조회
BE-->>UF: authorization_pending
end
User->>Approver: 링크 열기 또는 코드 검증
Approver->>BE: magic-link/code verify
BE->>Redis: pendingRef/status=approved, sessionJwt 저장
UF->>BE: poll 재시도
BE-->>UF: sessionJwt 반환
```
### 4.2 Magic Link Verify
Magic Link Verify는 전달된 토큰을 검증하여 로그인 또는 승인만 수행하는 흐름이다.
대표 흐름은 다음과 같다.
1. Backend가 magic token을 발급하고 Redis에 저장
2. 사용자가 링크를 클릭
3. UserFront가 `POST /api/v1/auth/magic-link/verify` 호출
4. Backend가 token을 조회하고 만료 여부 확인
5. `verifyOnly` 여부에 따라 즉시 세션 발급 또는 원래 pending 요청 승인
주요 데이터는 다음과 같다.
| 데이터 | 의미 |
| --- | --- |
| `token` | 링크에 포함된 일회성 검증 토큰 |
| `pendingRef` | 원래 로그인 요청이 있는 경우 연결되는 대기 키 |
| `verifyOnly` | 현재 창에서 로그인하지 않고 원래 요청만 승인할지 여부 |
| `approverSubject` | 승인을 수행한 사용자 identity |
이 방식은 사용자가 링크를 누르는 위치에 따라 동작이 달라질 수 있다. 예를 들어 같은 브라우저에서 바로 로그인할 수도 있고, 다른 기기에서 원래 요청만 승인할 수도 있다.
그림으로 보면 다음과 같다.
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant BE as Baron Backend
participant Redis as Redis
participant UF as UserFront
participant Req as 원래 요청 브라우저
BE->>Redis: token -> loginId/pendingRef 저장
BE-->>User: Magic Link 전달
User->>UF: Magic Link 클릭
UF->>BE: POST /api/v1/auth/magic-link/verify
BE->>Redis: token 조회 및 만료 확인
alt verifyOnly=true
BE->>Redis: 원래 pendingRef 승인 처리
Req->>BE: poll
BE-->>Req: sessionJwt 또는 승인 결과 반환
else 현재 브라우저 로그인
BE-->>UF: token/sessionJwt 반환
end
```
### 4.3 QR 로그인
QR 로그인은 요청 기기에서 QR을 표시하고, 이미 로그인된 승인 기기에서 QR을 스캔하여 요청 기기를 로그인시키는 흐름이다.
대표 흐름은 다음과 같다.
1. 요청 기기에서 UserFront가 `POST /api/v1/auth/qr/init` 호출
2. Backend가 `pendingRef`와 QR payload 생성
3. 요청 기기는 QR을 표시하고 `POST /api/v1/auth/qr/poll`로 상태 폴링
4. 승인 기기는 QR을 스캔하고 `POST /api/v1/auth/qr/approve` 호출
5. Backend가 승인 기기의 현재 세션을 확인
6. 승인된 subject를 기준으로 pending 상태를 승인 처리
7. 요청 기기의 poll 응답으로 `sessionJwt`가 내려가고 로그인 완료
주요 데이터는 다음과 같다.
| 데이터 | 의미 |
| --- | --- |
| `pendingRef` | QR 로그인 요청 식별자 |
| `qrCode` | 요청 기기에 표시되는 QR 데이터 |
| `approverSessionID` | QR을 승인한 기기의 세션 ID |
| `approverSubject` | QR을 승인한 사용자 |
| `requestDevice` | QR을 띄운 기기의 IP, User-Agent, 대략적 위치 |
QR 로그인은 편의성이 높지만, 타인이 QR을 띄우고 사용자가 실수로 승인하는 상황을 반드시 방어해야 한다.
그림으로 보면 다음과 같다.
```mermaid
sequenceDiagram
autonumber
participant Web as 요청 기기
participant BE as Baron Backend
participant Redis as Redis
participant Mobile as 승인 기기
Web->>BE: POST /api/v1/auth/qr/init
BE->>Redis: pendingRef/status=pending 저장
BE-->>Web: qrCode, pendingRef 반환
loop QR 상태 폴링
Web->>BE: POST /api/v1/auth/qr/poll
BE->>Redis: pendingRef 상태 조회
BE-->>Web: authorization_pending
end
Mobile->>Web: QR 스캔
Mobile->>BE: POST /api/v1/auth/qr/approve
BE->>BE: 승인 기기의 Kratos 세션 확인
BE->>Redis: pendingRef/status=approved, approverSubject 저장
Web->>BE: poll 재시도
BE-->>Web: sessionJwt 반환
```
## 5. 비표준 방식의 공통 위험 지점
비표준 방식의 핵심 위험은 "요청 주체"와 "승인 주체"가 다를 수 있다는 데 있다.
예를 들어 공격자가 자기 PC에서 로그인 링크 또는 QR 요청을 만들고, 실제 사용자에게 승인 행동을 유도할 수 있다. 이때 사용자가 아무 정보 없이 승인하면 공격자의 기기가 로그인될 수 있다.
따라서 비표준 방식에는 다음 데이터가 필수로 남아야 한다.
- 요청 생성 시각
- 요청 만료 시각
- 요청 IP
- 요청 User-Agent
- 요청 기기 유형
- 요청 위치 추정값
- 요청 RP 또는 client ID
- 승인 사용자 subject
- 승인 세션 ID
- 승인 기기 IP/User-Agent
- 승인 또는 거절 결과
## 6. 사용자가 아닌 사람의 로그인 요청 발견 시 처리 방안
### 6.1 기본 원칙
타인의 로그인 요청 가능성이 있는 경우, 시스템은 사용자가 판단할 수 있는 정보를 보여주고 명확한 선택지를 제공해야 한다.
사용자에게 제공할 선택지는 최소 3개가 적절하다.
| 사용자 선택 | 의미 | 처리 |
| --- | --- | --- |
| 내 요청입니다 | 사용자가 요청을 인지하고 승인 | 제한 조건 검증 후 승인 |
| 내 요청이 아닙니다 | 타인의 요청으로 판단 | 즉시 차단, pending 폐기, 보안 이벤트 기록 |
| 잘 모르겠습니다 | 확신 불가 | 기본 차단 또는 보류, 추가 확인 유도 |
보안 관점의 기본값은 "비차단"이 아니라 "보류 또는 차단"이어야 한다.
### 6.2 사용자 알림 방식
의심 요청이 생성되거나 승인 직전에 다음 정보를 사용자에게 보여준다.
- 요청 서비스: 예) Baron SSO, DevFront, AdminFront, 특정 RP
- 요청 시간
- 요청 기기: 브라우저, OS, 모바일/데스크톱 추정
- 요청 위치: 국가/지역 수준의 추정값
- 요청 IP 일부 마스킹
- 승인 시 결과: "이 요청을 승인하면 해당 기기가 로그인됩니다."
알림 채널은 상황에 따라 다르게 적용한다.
| 상황 | 권장 알림 |
| --- | --- |
| QR 승인 화면 | 승인 화면에 요청 정보 직접 표시 |
| Magic Link 클릭 | 링크 검증 화면에 요청 정보 표시 |
| Enchanted Link 승인 | 승인 화면 또는 poll 연결 화면에 요청 정보 표시 |
| 고위험 요청 | 이메일/SMS/앱 알림 병행 |
### 6.3 단계별 처리 흐름
#### 1단계: 요청 생성
Backend가 pending 요청을 만들 때 요청 메타데이터를 함께 저장한다.
저장 예시는 다음과 같다.
```json
{
"pendingRef": "abc123",
"loginId": "user@example.com",
"requestIp": "203.0.113.xxx",
"requestUserAgent": "Chrome on Windows",
"requestDeviceType": "desktop",
"requestClientId": "userfront",
"requestedAt": "2026-06-22T10:00:00+09:00",
"expiresAt": "2026-06-22T10:05:00+09:00",
"status": "pending"
}
```
#### 2단계: 위험도 평가
요청 생성 시 또는 승인 직전에 risk score를 계산한다.
위험도를 높이는 조건은 다음과 같다.
- 최근 로그인 위치와 크게 다른 위치
- 새 기기 또는 새 브라우저
- 짧은 시간 내 반복 요청
- 실패한 비밀번호 시도 직후 링크/QR 요청
- 관리자 계정 또는 권한 높은 계정
- 비활성/잠금/퇴사 상태 사용자
- RP client가 inactive 또는 미승인 상태
#### 3단계: 사용자 확인
승인 화면에서 다음 문구를 제공한다.
```text
새 로그인 요청이 감지되었습니다.
요청 기기: Chrome on Windows
요청 위치: Seoul, KR
요청 시간: 2026-06-22 10:00
요청 서비스: Baron SSO
이 요청을 승인하면 해당 기기가 로그인됩니다.
본인이 요청한 로그인이 맞습니까?
```
버튼은 다음처럼 구성한다.
- `내 요청입니다`
- `내 요청이 아닙니다`
- `잘 모르겠습니다`
#### 4단계: 차단 선택 처리
사용자가 `내 요청이 아닙니다`를 선택하면 다음 순서로 처리한다.
1. `pendingRef` 상태를 `blocked`로 변경
2. 해당 pending 요청에서 세션 발급 금지
3. poll 중인 요청 기기에는 `blocked_by_user` 응답 반환
4. 감사 로그에 `login.request.blocked_by_user` 기록
5. 사용자에게 비밀번호 변경 또는 전체 세션 로그아웃 안내
6. 반복 발생 시 계정 보호 모드 전환 검토
요청 기기에는 구체적인 사용자 정보를 노출하지 않는다. 예시는 다음 정도가 적절하다.
```json
{
"error": "login_request_blocked",
"code": "blocked_by_user",
"message": "This login request was not approved."
}
```
#### 5단계: 비차단 선택 처리
사용자가 `내 요청입니다`를 선택하면 바로 승인하지 않고 최소 검증을 한 번 더 수행한다.
검증 항목은 다음과 같다.
- pending 요청이 만료되지 않았는지
- 승인 사용자의 subject와 로그인 대상 subject가 같은지
- 기존 브라우저 세션과 대상 subject가 충돌하지 않는지
- RP client가 활성 상태인지
- 사용자 상태가 로그인 가능 상태인지
검증 통과 후에만 세션 발급 또는 Hydra `AcceptLoginRequest`를 수행한다.
#### 6단계: 보류 선택 처리
사용자가 `잘 모르겠습니다`를 선택하면 기본적으로 승인하지 않는다.
처리 방식은 다음 중 하나가 적절하다.
- 보안 우선: 즉시 `blocked_uncertain` 처리
- UX 우선: `hold` 상태로 바꾸고 짧은 시간 뒤 만료
권장안은 보안 우선이다. 사용자가 모르는 요청은 사실상 승인하면 안 된다.
## 7. 구현 검토 항목
### 7.1 데이터 모델 또는 Redis 상태 확장
비표준 로그인 pending 상태에 아래 필드를 공통으로 포함하는 것이 좋다.
- `requestIp`
- `requestUserAgent`
- `requestDeviceType`
- `requestGeo`
- `requestClientId`
- `requestedAt`
- `expiresAt`
- `riskLevel`
- `riskReasons`
- `approvalDecision`
- `approvalSubject`
- `approvalSessionID`
- `approvalIp`
- `approvalUserAgent`
### 7.2 API 응답 표준화
poll 계열 API는 상태를 명확히 분리해야 한다.
| 상태 | 의미 |
| --- | --- |
| `authorization_pending` | 아직 승인 전 |
| `approved` | 승인됨, 세션 발급 가능 |
| `blocked_by_user` | 사용자가 차단 |
| `blocked_by_policy` | 정책상 차단 |
| `expired_token` | 만료 |
| `slow_down` | 폴링 속도 제한 |
### 7.3 감사 로그
최소 이벤트는 다음과 같다.
- `login.request.created`
- `login.request.viewed_by_user`
- `login.request.approved_by_user`
- `login.request.blocked_by_user`
- `login.request.blocked_by_policy`
- `login.request.expired`
- `login.session.issued`
### 7.4 사용자 화면
QR, Magic Link, Enchanted Link 승인 화면은 공통 컴포넌트를 사용할 수 있다.
공통 컴포넌트가 표시해야 할 정보는 다음과 같다.
- 요청 서비스
- 요청 기기
- 요청 위치
- 요청 시간
- 승인 결과 설명
- 차단/승인/보류 버튼
## 8. 결론
Baron SSO의 로그인 방식은 표준 2개와 비표준 3개로 정리할 수 있다.
- 표준 방식은 Kratos의 password/code login을 기반으로 하므로 이해 지점은 Kratos flow와 세션 발급이다.
- 비표준 방식은 Baron이 pendingRef, Redis 상태, 폴링, 교차 기기 승인을 추가한 구조이므로 요청자와 승인자를 분리해서 이해해야 한다.
- 사용자가 아닌 사람의 로그인 요청을 막으려면 승인 직전에 요청 메타데이터를 사용자에게 보여주고, `내 요청입니다`, `내 요청이 아닙니다`, `잘 모르겠습니다` 선택을 받아 상태 머신으로 처리해야 한다.
가장 중요한 정책은 다음 한 문장으로 요약된다.
> 사용자가 명확히 본인 요청이라고 확인하지 않은 비표준 로그인 요청은 세션 발급으로 이어지면 안 된다.
## 9. 최근 운영 이슈와의 연관성
최근 운영 저장소 이슈 중 로그인 방식 검토와 직접 연결되는 항목은 다음과 같다.
| 이슈 | 연관도 | 검토 포인트 |
| --- | --- | --- |
| `#1249` Naver Works 버튼 기반 로그아웃 요청 | 매우 높음 | 로그인 성공 알림 후 사용자가 "내가 아닌 로그인"을 선택하면 refresh grant revoke로 세션을 끊는 구조이다. 본 문서의 차단 처리 흐름과 직접 연결된다. |
| `#1248` 휴대폰 번호 입력만으로 즉시 로그인 | 매우 높음 | 전화번호 하나만으로 세션을 만드는 설계이다. Kratos code flow를 활용하더라도 SMS 발송과 사용자 코드 입력을 생략하므로 고위험 비표준 흐름으로 분류해야 한다. |
| `#1247` Baron Backend와 Ory Kratos 인터랙션 분석 | 매우 높음 | Headless 방식, PKCE 방식, Courier 인터셉트 패턴을 설명한다. 표준/비표준 경계와 데이터 흐름 검토의 핵심 자료이다. |
| `#1246` 로그아웃 처리 방식 refresh revoke 전환 | 높음 | 타인 로그인 또는 의심 로그인 발견 후 세션을 종료하는 후속 조치와 연결된다. polling/Redis 기반보다 refresh revoke 기반이 최신 방향으로 보인다. |
| `#1245` QR/링크 로그인 분석 및 Headless 폰번호 적용 | 매우 높음 | 기존 비표준 방식인 QR/링크 흐름을 Headless 폰번호 로그인에 재사용하려는 설계이다. |
| `#1241` Headless 폰번호 전용 로그인 API | 매우 높음 | `client_assertion`, `phoneNumber`, `login_challenge`만으로 OIDC 로그인을 완료하는 API이다. 사용자 실재 인증 여부를 별도로 검토해야 한다. |
| `#1242` 세션 폐기 Redis 버퍼링 | 중간 | 세션 폐기 이벤트를 RP가 감지하게 하는 구조이나, `#1246`의 refresh revoke 방식과 정책 충돌 여부를 확인해야 한다. |
| `#1244` DevFront 로그인 후 언어 전환 이슈 | 낮음 | 로그인 이후 UX 문제이며 인증 흐름 자체와 직접 관련은 낮다. |
| `#1250` 인력정보 백업 및 스테이징 복구 | 중간 | Kratos identity 포함 여부와 credential/session reset policy가 로그인 데이터 관리와 간접 연결된다. |
## 10. Headless 폰번호 로그인 추가 검토
`#1247`, `#1248`, `#1241`에서 제안된 Headless 폰번호 로그인은 다음 흐름을 가진다.
전체 흐름을 그림으로 보면 다음과 같다.
```mermaid
sequenceDiagram
autonumber
actor User as 사용자
participant RP as RP/시스템
participant GW as Nginx Gateway
participant BE as Baron Backend
participant Redis as Redis
participant OK as Oathkeeper
participant KR as Ory Kratos
participant HY as Ory Hydra
User->>RP: 전화번호 입력 후 로그인 요청
RP->>GW: POST /api/v1/auth/headless/phone/login
Note over RP,GW: body 예시
client_id=headless-phone-client
phoneNumber=010-1234-5678
login_challenge=abc...
client_assertion=private_key_jwt
GW->>BE: /api 경로를 Backend로 proxy
BE->>BE: RP 서명 검증 및 전화번호 정규화
Note over BE: phoneNumber 정규화
010-1234-5678 -> +821012345678
BE->>Redis: phone pending/interception flag 저장
Note over BE,Redis: key 예시
prefixLoginCodePhonePending:+821012345678
TTL=5m
BE->>KR: InitiateLinkLogin(phoneNumber)
KR-->>BE: flowID 반환
BE->>Redis: phoneNumber -> flowID 저장
Note over BE,Redis: key 예시
prefixLoginCode:+821012345678 -> flowID
KR->>BE: Courier SMS 발송 웹훅 호출
Note over KR,BE: POST /api/v1/auth/webhooks/kratos-courier
recipient=+821012345678
template_data.login_code=123456
BE->>Redis: interception flag 확인
BE->>BE: Courier payload에서 login_code 추출
BE->>KR: VerifyLoginCode(phoneNumber, flowID, login_code)
KR-->>BE: Kratos session_token/session_id 발급
BE->>HY: AcceptLoginRequest(login_challenge, subject)
HY-->>BE: redirectTo 반환
BE-->>GW: redirectTo, sessionId, status 반환
GW-->>RP: JSON 응답 전달
RP->>GW: redirectTo 따라 /oidc/oauth2/auth 계속
GW->>OK: /oidc 경로를 Oathkeeper로 proxy
OK->>HY: strip /oidc 후 Hydra public으로 전달
```
이때 흐름 속 주요 데이터 예시는 다음과 같다.
RP가 Backend로 보내는 요청:
```json
{
"client_id": "headless-phone-client",
"client_assertion": "eyJhbGciOiJSUzI1NiIs...",
"phoneNumber": "010-1234-5678",
"login_challenge": "b59a857064ef4827940bb..."
}
```
Backend가 Redis에 저장하는 임시 키:
```text
prefixLoginCodePhonePending:+821012345678 = interception_flag
prefixLoginCode:+821012345678 =
TTL = 5m
```
Kratos Courier가 Backend로 보내는 발송 요청의 핵심 데이터:
```json
{
"recipient": "+821012345678",
"template_type": "login_code",
"template_data": {
"login_code": "123456"
}
}
```
Backend가 RP로 반환하는 성공 응답:
```json
{
"status": "ok",
"redirectTo": "https://sso.example.com/oidc/oauth2/auth?login_verifier=...",
"sessionId": "a0f8dc4e-9400-4785-802c-..."
}
```
### 10.1 단계별 데이터 이동
#### Step 1. 인가 및 전화번호 식별
RP가 Baron Backend에 다음 데이터를 전달한다.
- `client_id`
- `client_assertion`: RP의 private key JWT 서명값
- `phoneNumber`: 사용자가 입력한 전화번호
- `login_challenge`: Hydra OIDC 로그인 요청 식별자
Backend는 먼저 RP가 폰번호 전용 로그인을 사용할 수 있는 기등록 클라이언트인지 검증한다. 이후 전화번호를 E.164 형식으로 정규화한다.
예시:
```text
010-1234-5678 -> +821012345678
```
그 다음 정규화된 전화번호가 Baron/Kratos 원장에 존재하는지 확인한다.
#### Step 2. 인터셉터 플래그 캐싱
Backend는 Redis에 인증 발송 차단 또는 내부 처리용 플래그를 저장한다.
```text
prefixLoginCodePhonePending:+821012345678 -> interception_flag
TTL: 5m
```
이 플래그는 Kratos가 Courier를 통해 SMS 발송을 시도할 때, 실제 외부 SMS 발송으로 보내지 않고 Backend 내부 처리 흐름으로 분기시키기 위한 신호 역할을 한다.
#### Step 3. 대리 인증 흐름 기동
Backend는 Kratos code login flow를 시작한다.
```text
h.IdpProvider.InitiateLinkLogin("+821012345678", returnTo)
```
Kratos는 내부 login flow를 만들고 `flowID`를 반환한다. Backend는 이를 Redis에 저장한다.
```text
prefixLoginCode:+821012345678 -> flowID
TTL: 5m
```
#### Step 4. SMS 인터셉트 및 원격 인증 자동 수립
Kratos Courier가 SMS 발송 웹훅을 Backend로 호출한다.
```text
POST /api/v1/auth/kratos-courier-relay
```
Backend는 수신 번호에 `prefixLoginCodePhonePending` 플래그가 있는지 확인한다. 플래그가 있으면 외부 SMS 발송을 수행하지 않고, Courier payload 안의 `login_code`를 추출한다.
이후 Backend는 Redis에서 `flowID`를 조회하고 Kratos에 code 검증을 제출한다.
```text
VerifyLoginCode("+821012345678", flowID, login_code)
```
검증이 성공하면 Kratos가 공식 세션을 발급한다. Backend는 Kratos Courier에 200 OK를 반환하여 Courier 큐를 정상 종료시킨다.
#### Step 5. OIDC 리다이렉트 합의 및 RP 응답
Backend는 확보한 subject/session 정보를 바탕으로 Hydra login request를 승인한다.
```text
Hydra.AcceptLoginRequest(login_challenge, subject)
```
성공하면 RP에 다음 정보를 반환한다.
- `redirectTo`
- `sessionId`
- `status`
RP 또는 브라우저는 `redirectTo`를 따라가며 OIDC 흐름을 완료한다.
### 10.2 표준 여부 판단
이 흐름은 Kratos의 code login과 Hydra OIDC를 사용한다는 점에서는 표준 컴포넌트를 재사용한다. 그러나 사용자 관점에서 핵심 인증 행위인 SMS 수신, 코드 입력, 명시적 승인 단계가 Backend 내부 자동 처리로 대체된다.
따라서 이 방식은 "Ory 컴포넌트 기반"이라고 표현할 수는 있지만, "Ory 보안 표준 100% 준수" 또는 "취약점이 전혀 없음"이라고 표현하면 안 된다. Baron이 별도 신뢰 경계와 위험을 만든 비표준 고위험 로그인 방식으로 분류하는 것이 적절하다.
### 10.3 주요 위험
이 설계의 위험은 다음과 같다.
- 전화번호를 아는 것만으로 로그인 요청이 시작될 수 있다.
- RP의 `client_assertion`이 유출되거나 오용되면 대량 로그인 시도 통로가 될 수 있다.
- SMS가 실제 사용자에게 전달되지 않기 때문에 사용자가 로그인 시도를 인지하지 못할 수 있다.
- Courier payload의 `login_code`를 Backend가 내부 자동 검증하는 구조는 감사와 권한 경계가 매우 중요하다.
- 사용자의 명시적 승인 없이 Hydra `AcceptLoginRequest`까지 자동 처리되면 "누가 로그인 요청을 승인했는가"가 모호해질 수 있다.
### 10.4 필수 통제 조건
Headless 폰번호 로그인을 유지하거나 구현하려면 최소한 다음 조건이 필요하다.
- 허용 RP allowlist
- RP별 `headless_phone_login_enabled` 명시 설정
- `private_key_jwt` 기반 `client_assertion` 검증
- JWKS 캐시 및 키 회전 정책
- 전화번호 enumeration 방지 응답 정책
- IP, client, phoneNumber 기준 rate limit
- 고위험 계정 또는 관리자 계정 사용 금지
- 로그인 성공 즉시 사용자 알림
- 사용자의 "내가 아닌 로그인" 선택 시 refresh grant revoke
- 모든 단계의 감사 로그 기록
### 10.5 용어 정리
설계 문서나 보고서에는 "갈취"라는 표현 대신 다음 용어를 사용하는 것이 적절하다.
| 피해야 할 표현 | 권장 표현 |
| --- | --- |
| 코드 갈취 | Courier payload에서 login_code 추출 |
| SMS 탈취 | Courier 발송 요청 인터셉트 |
| 강제 세션 생성 | Kratos code 검증을 통한 세션 발급 |
| 완전 차단 | 정책 기반 차단 또는 위험 완화 |
| 취약점이 전혀 없음 | 표준 컴포넌트를 재사용하나 별도 위험 통제 필요 |
## 11. 팀장 보고용 핵심 결론
팀장 보고 시에는 다음처럼 정리하는 것이 안전하다.
1. Baron SSO의 기존 로그인 방식은 표준 2개, 비표준 3개로 분류된다.
2. 최근 이슈의 Headless 폰번호 로그인은 기존 비표준 방식보다 더 강한 비표준 확장이다.
3. Kratos/Hydra를 사용한다고 해서 전체 흐름이 자동으로 표준 인증이 되는 것은 아니다.
4. 사용자가 직접 인증 코드 입력 또는 승인 행동을 하지 않는 흐름은 반드시 별도 보안 통제와 감사 로그가 필요하다.
5. "내가 아닌 로그인" 발견 시에는 Naver Works 알림, 사용자 차단 선택, refresh grant revoke, 감사 로그 기록까지 하나의 폐쇄 루프로 설계해야 한다.