784 lines
31 KiB
Markdown
784 lines
31 KiB
Markdown
# 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 예시<br/>client_id=headless-phone-client<br/>phoneNumber=010-1234-5678<br/>login_challenge=abc...<br/>client_assertion=private_key_jwt
|
|
GW->>BE: /api 경로를 Backend로 proxy
|
|
BE->>BE: RP 서명 검증 및 전화번호 정규화
|
|
Note over BE: phoneNumber 정규화<br/>010-1234-5678 -> +821012345678
|
|
BE->>Redis: phone pending/interception flag 저장
|
|
Note over BE,Redis: key 예시<br/>prefixLoginCodePhonePending:+821012345678<br/>TTL=5m
|
|
BE->>KR: InitiateLinkLogin(phoneNumber)
|
|
KR-->>BE: flowID 반환
|
|
BE->>Redis: phoneNumber -> flowID 저장
|
|
Note over BE,Redis: key 예시<br/>prefixLoginCode:+821012345678 -> flowID
|
|
KR->>BE: Courier SMS 발송 웹훅 호출
|
|
Note over KR,BE: POST /api/v1/auth/webhooks/kratos-courier<br/>recipient=+821012345678<br/>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 = <kratos-flow-id>
|
|
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, 감사 로그 기록까지 하나의 폐쇄 루프로 설계해야 한다.
|