# 비표준 로그인 승인 화면 공통화 개발안

작성일: 2026-06-23

## 1. 목적

본 문서는 Baron SSO의 비표준 로그인 방식인 로그인 링크, Magic Link, QR 로그인의 승인 화면을 공통화하기 위한 단계별 개발안을 정의한다.

목표는 다음과 같다.

- 사용자가 승인 전에 "어떤 기기가 로그인되려는지" 쉽게 확인하도록 한다.
- 링크/QR/Magic Link마다 흩어진 승인 UX를 하나의 공통 패턴으로 정리한다.
- 기존 로그인 동작에 영향을 주지 않도록 opt-in 방식으로 개발한다.
- 개발 후 기존 표준 로그인, 링크 로그인, QR 로그인, OIDC 흐름에 영향이 없는지 반드시 확인한다.

## 2. 현재 상태 요약

현재 코드 기준 관련 위치는 다음과 같다.

| 영역 | 파일 | 현재 역할 |
| --- | --- | --- |
| UserFront API client | `userfront/lib/core/services/auth_proxy_service.dart` | 링크, Magic Link, QR init/poll/approve API 호출 |
| QR 승인 화면 | `userfront/lib/features/auth/presentation/approve_qr_screen.dart` | QR 스캔 후 자동 승인 처리 |
| 로그인 화면 | `userfront/lib/features/auth/presentation/login_screen.dart` | 비밀번호, 링크, QR 로그인 UI 및 poll 처리 |
| 라우팅 | `userfront/lib/main.dart` | `/ql/:ref`, `/qr/approve` 계열 라우트 연결 |
| Backend route | `backend/cmd/server/main.go` | `/api/v1/auth/qr/approve`, `/magic-link/verify`, `/enchanted-link/poll` 등록 |
| Backend handler | `backend/internal/handler/auth_handler.go` | QR/link pending 상태, Redis 저장, 감사 로그 처리 |

이미 존재하는 재사용 가능 코드도 있다.

| 기존 코드 | 재사용 이유 |
| --- | --- |
| `storeQrMeta`, `loadQrMeta` | QR 요청 기기의 IP/User-Agent 저장 구조가 이미 있다. |
| `storeLoginApproverMeta`, `loadLoginApproverMeta` | 링크 승인자의 IP/User-Agent/session 정보를 저장하는 구조가 이미 있다. |
| `writeQrAuditLog`, `writeLinkAuditLog` | 승인 성공 감사 로그가 이미 있다. |
| `AuthProxyService.approveQrLogin` | QR 승인 API client가 이미 있다. |
| `AuthProxyService.verifyMagicLink`, `verifyLoginCode` | 링크/Magic Link 승인 API client가 이미 있다. |

## 3. 개발 원칙

이번 개발은 기존 소스코드 영향도를 최소화하기 위해 아래 원칙을 따른다.

1. 기존 API 응답 형식을 즉시 변경하지 않는다.
2. 기존 QR 자동 승인 화면을 바로 제거하지 않는다.
3. 새 공통 승인 화면은 feature flag가 켜졌을 때만 사용한다.
4. Backend에는 조회용 metadata API를 먼저 추가하고, 기존 승인 API는 최대한 유지한다.
5. 공통 UI는 기존 API client 위에 얇게 얹고, 성공/실패 동작은 기존 흐름과 동일하게 유지한다.
6. 차단 버튼은 1차 개발에서는 UI placeholder 또는 별도 후속 단계로 분리할 수 있다.
7. 개발 후에는 기존 자동 승인 흐름과 새 수동 승인 흐름을 모두 테스트한다.

## 4. 목표 화면

공통 승인 화면은 다음 정보를 표시한다.

```text
새 로그인 요청

요청 서비스: Baron SSO 또는 RP 이름
요청 방식: QR 로그인 / 로그인 링크 / Magic Link
요청 기기: Chrome on Windows
요청 위치/IP: 203.0.113.xxx
요청 시간: 2026-06-23 10:30

이 요청을 승인하면 해당 기기가 로그인됩니다.
본인이 요청한 로그인이 맞습니까?

[내 요청입니다] [내 요청이 아닙니다] [잘 모르겠습니다]
```

1차 개발에서는 버튼 동작을 다음처럼 나눈다.

| 버튼 | 1차 처리 | 후속 처리 |
| --- | --- | --- |
| 내 요청입니다 | 기존 승인 API 호출 | 유지 |
| 내 요청이 아닙니다 | pending block API가 있으면 호출, 없으면 안전 안내 후 승인하지 않음 | `blocked_by_user` 상태 저장 구현 |
| 잘 모르겠습니다 | 승인하지 않고 안내 화면 표시 | `blocked_uncertain` 또는 `hold` 상태 구현 |

## 5. Backend 개발안

### 5.1 새 응답 모델 추가

Backend에 공통 승인 요청 metadata 응답 모델을 추가한다.

권장 위치:

```text
backend/internal/domain/auth_models.go
```

예시:

```go
type LoginApprovalRequestInfo struct {
    PendingRef        string `json:"pendingRef"`
    ApprovalType      string `json:"approvalType"` // qr, link, magic_link
    Status            string `json:"status"`       // pending, approved, success, expired, blocked
    RequestService    string `json:"requestService,omitempty"`
    RequestClientID   string `json:"requestClientId,omitempty"`
    RequestIPAddress  string `json:"requestIpAddress,omitempty"`
    RequestUserAgent  string `json:"requestUserAgent,omitempty"`
    RequestDevice     string `json:"requestDevice,omitempty"`
    RequestedAt       string `json:"requestedAt,omitempty"`
    ExpiresAt         string `json:"expiresAt,omitempty"`
    Message           string `json:"message,omitempty"`
}
```

주의:

- `requestIpAddress`는 전체 IP를 그대로 노출하지 않고 마스킹한다.
- `requestUserAgent`는 원문과 표시용을 분리하는 것이 좋다.
- `pendingRef`가 유효하지 않아도 사용자 존재 여부를 추론할 수 있는 메시지를 주지 않는다.

### 5.2 공통 승인 정보 조회 API 추가

새 API를 추가한다.

```text
POST /api/v1/auth/approval/info
```

요청:

```json
{
  "pendingRef": "abc123",
  "approvalType": "qr"
}
```

응답:

```json
{
  "pendingRef": "abc123",
  "approvalType": "qr",
  "status": "pending",
  "requestService": "Baron SSO",
  "requestClientId": "userfront",
  "requestIpAddress": "203.0.113.xxx",
  "requestUserAgent": "Chrome on Windows",
  "requestDevice": "Desktop browser",
  "requestedAt": "2026-06-23T10:30:00+09:00",
  "expiresAt": "2026-06-23T10:35:00+09:00"
}
```

구현 위치:

```text
backend/cmd/server/main.go
backend/internal/handler/auth_handler.go
```

route 추가:

```go
auth.Post("/approval/info", authHandler.GetLoginApprovalInfo)
```

처리 방식:

1. `pendingRef`, `approvalType`을 받는다.
2. `approvalType=qr`이면 `loadQrMeta(pendingRef)`를 우선 사용한다.
3. `approvalType=link` 또는 `magic_link`이면 `loadLoginMeta(pendingRef)`와 기존 Redis 상태를 조회한다.
4. Redis의 `prefixSession + pendingRef` 상태를 확인한다.
5. 표시 가능한 정보만 응답한다.

영향도:

- 신규 API 추가만 수행하므로 기존 API 동작에 영향 없음.
- 기존 `/qr/approve`, `/magic-link/verify`, `/login/code/verify`는 변경하지 않는다.

### 5.3 요청 metadata 저장 보강

QR은 이미 `storeQrMeta(pendingRef, c)`가 있다.

링크 계열은 init 시점의 요청자 metadata 저장이 부족할 수 있으므로, 별도 공통 metadata 저장 함수를 추가한다.

권장 함수:

```go
func (h *AuthHandler) storeApprovalRequestMeta(pendingRef, approvalType, clientID string, c *fiber.Ctx, ttl time.Duration)
func (h *AuthHandler) loadApprovalRequestMeta(pendingRef string) (approvalRequestMeta, bool)
```

권장 Redis key:

```text
approval_request_meta:{pendingRef}
```

1차 적용 범위:

- `InitQRLogin`: 기존 `storeQrMeta` 유지, 새 metadata도 병행 저장
- `InitEnchantedLink`: pendingRef 생성 직후 새 metadata 저장
- Magic Link는 token 자체보다 연결된 `pendingRef` 기준으로 metadata 조회

영향도 최소화 방법:

- 기존 `prefixQrMeta`는 제거하지 않는다.
- 새 `approval_request_meta`는 조회 실패해도 기존 동작을 막지 않는다.
- metadata 저장 실패는 로그인 실패로 처리하지 않는다.

### 5.4 block API는 2단계로 분리

1차 공통 화면에서 `내 요청이 아닙니다` 버튼은 승인하지 않는 것만으로도 기존 자동 승인보다 안전하다.

다만 최종적으로는 아래 API가 필요하다.

```text
POST /api/v1/auth/approval/block
```

요청:

```json
{
  "pendingRef": "abc123",
  "approvalType": "qr",
  "decision": "blocked_by_user"
}
```

응답:

```json
{
  "status": "blocked_by_user"
}
```

2단계 구현 시 처리:

1. `prefixSession + pendingRef`의 status를 `blocked_by_user`로 변경한다.
2. poll API가 `blocked_by_user`를 반환하게 한다.
3. 감사 로그 `login.request.blocked_by_user`를 남긴다.

1차 개발에서는 이 API를 만들되 feature flag로 UI 연결을 늦춰도 된다.

## 6. UserFront 개발안

### 6.1 공통 모델 추가

권장 파일:

```text
userfront/lib/features/auth/domain/login_approval_request.dart
```

예시 필드:

```dart
class LoginApprovalRequest {
  final String pendingRef;
  final String approvalType;
  final String status;
  final String? requestService;
  final String? requestClientId;
  final String? requestIpAddress;
  final String? requestUserAgent;
  final String? requestDevice;
  final DateTime? requestedAt;
  final DateTime? expiresAt;
}
```

영향도:

- 신규 파일 추가이므로 기존 화면 영향 없음.

### 6.2 AuthProxyService API 추가

권장 파일:

```text
userfront/lib/core/services/auth_proxy_service.dart
```

추가 함수:

```dart
static Future<Map<String, dynamic>> getLoginApprovalInfo({
  required String pendingRef,
  required String approvalType,
})
```

요청 대상:

```text
POST /api/v1/auth/approval/info
```

영향도:

- 기존 함수 수정 없이 새 함수만 추가한다.
- 기존 QR/link 로그인 호출 경로는 그대로 유지한다.

### 6.3 공통 승인 화면 추가

권장 파일:

```text
userfront/lib/features/auth/presentation/login_approval_screen.dart
```

props:

```dart
class LoginApprovalScreen extends StatefulWidget {
  final String pendingRef;
  final String approvalType; // qr, link, magic_link
  final bool autoApproveFallback;
}
```

화면 동작:

1. 진입 시 `AuthProxyService.getLoginApprovalInfo` 호출
2. 요청 정보 표시
3. `내 요청입니다` 클릭 시 approvalType별 기존 API 호출
4. 성공 시 기존 성공 화면 또는 dashboard 이동
5. `내 요청이 아닙니다`, `잘 모르겠습니다`는 세션 발급 없이 안내

approvalType별 기존 API 매핑:

| approvalType | 승인 API |
| --- | --- |
| `qr` | `AuthProxyService.approveQrLogin` |
| `magic_link` | `AuthProxyService.verifyMagicLink` |
| `link_code` | `AuthProxyService.verifyLoginCode` |

주의:

- Magic Link는 token 기반이므로 `pendingRef`만으로 승인할 수 없는 경우가 있다.
- 따라서 Magic Link 공통화는 token verify 화면의 wrapper로 시작하고, 1차에서는 QR부터 적용하는 것이 안전하다.

### 6.4 QR 승인 화면 점진 전환

현재 `ApproveQrScreen`은 진입 후 자동 승인한다.

1차 변경안:

```text
기존:
/ql/:ref -> ApproveQrScreen -> 자동 approveQrLogin

신규 feature flag ON:
/ql/:ref -> LoginApprovalScreen(approvalType=qr) -> 사용자가 직접 승인

feature flag OFF:
/ql/:ref -> ApproveQrScreen 유지
```

권장 feature flag:

```text
USERFRONT_COMMON_APPROVAL_SCREEN=true
```

또는 Flutter runtime env:

```text
VITE/ENV 성격에 맞는 userfront runtime env key로 추가
```

영향도 최소화:

- 기존 `ApproveQrScreen`은 삭제하지 않는다.
- 라우트에서 feature flag만 보고 새 화면으로 분기한다.
- 문제가 있으면 flag를 끄고 즉시 기존 자동승인으로 복귀한다.

### 6.5 링크/Magic Link 적용 순서

링크와 Magic Link는 QR보다 변형이 많다. 따라서 QR 적용 후 안정화되면 다음 순서로 진행한다.

1. QR 수동 승인 화면 적용
2. Magic Link verify 화면에 요청 정보 표시만 추가
3. Enchanted Link 승인 흐름에 공통 요청 정보 표시
4. `내 요청이 아닙니다` block API 연결

## 7. 단계별 개발 순서

### Phase 0. 기준 테스트 확보

목표:

- 개발 전 현재 동작이 정상임을 확인한다.

확인 항목:

1. 비밀번호 로그인 정상
2. 링크 로그인 init/poll 정상
3. Magic Link verify 정상
4. QR init/poll/approve 정상
5. OIDC login_challenge가 있는 로그인 정상

권장 명령:

```bash
go test ./backend/internal/handler -run 'Test.*QR|Test.*Link|Test.*LoginCode|Test.*Password'
flutter test
npm run test -- tests/orgfront-auto-login.spec.ts --project=chromium
```

프로젝트 환경에 따라 실제 명령은 조정한다.

### Phase 1. Backend 조회 API만 추가

작업:

1. `LoginApprovalRequestInfo` 모델 추가
2. `GetLoginApprovalInfo` handler 추가
3. `POST /api/v1/auth/approval/info` route 추가
4. QR metadata 조회 테스트 추가
5. 잘못된 pendingRef 응답 테스트 추가

영향도:

- 신규 API만 추가되므로 기존 로그인 흐름 영향 없음.

완료 기준:

- 기존 테스트 모두 통과
- 신규 API 테스트 통과
- 기존 QR approve API 응답 변경 없음

### Phase 2. Backend metadata 저장 보강

작업:

1. `approval_request_meta:{pendingRef}` Redis 저장 함수 추가
2. `InitQRLogin`에 병행 저장 추가
3. `InitEnchantedLink`에 병행 저장 추가
4. 저장 실패가 로그인 실패로 이어지지 않게 처리

영향도:

- Redis key만 추가된다.
- 기존 key 삭제/변경 없음.

완료 기준:

- Redis metadata 미저장 상황에서도 로그인 성공
- QR/link 기존 poll/approve 정상

### Phase 3. UserFront 공통 화면 신규 추가

작업:

1. `LoginApprovalRequest` 모델 추가
2. `AuthProxyService.getLoginApprovalInfo` 추가
3. `LoginApprovalScreen` 신규 추가
4. QR 승인용 UI 연결 로직 작성
5. feature flag 기본값 OFF

영향도:

- 기본값 OFF이므로 배포해도 기존 UX 유지
- 새 화면은 직접 route 또는 테스트에서만 접근

완료 기준:

- feature flag OFF에서 기존 QR 자동 승인 동작 동일
- feature flag ON에서 새 승인 화면 표시
- 승인 버튼 클릭 시 기존 `approveQrLogin` 호출

### Phase 4. QR 라우트 opt-in 연결

작업:

1. `userfront/lib/main.dart`의 QR approve route에서 feature flag 분기
2. flag ON이면 `LoginApprovalScreen(approvalType: 'qr')`
3. flag OFF이면 기존 `ApproveQrScreen`

영향도:

- flag OFF 기준 영향 없음.

완료 기준:

- flag OFF e2e: 기존 QR 로그인 성공
- flag ON e2e: 요청 정보 확인 후 수동 승인 성공
- 미로그인 승인 기기는 기존처럼 로그인 유도

### Phase 5. 링크/Magic Link 화면 확장

작업:

1. Magic Link verify 진입 화면에서 approval info 조회
2. 가능한 경우 요청 정보 표시
3. 기존 verify API 호출은 그대로 유지
4. Enchanted Link code verify에도 같은 UI wrapper 적용 검토

영향도:

- token 기반 링크는 기존 URL 처리와 충돌 가능성이 있어 QR보다 늦게 적용한다.

완료 기준:

- 기존 Magic Link URL이 계속 동작
- verifyOnly=true 흐름이 깨지지 않음
- pendingRef 없는 Magic Link도 기존처럼 처리

### Phase 6. block API 연결

작업:

1. `POST /api/v1/auth/approval/block` 추가
2. pending 상태 `blocked_by_user` 저장
3. QR/link poll에서 `blocked_by_user` 반환
4. UserFront에서 `내 요청이 아닙니다` 클릭 시 block 호출
5. 감사 로그 추가

영향도:

- poll 응답에 새 code가 추가된다.
- 기존 UserFront가 모르는 code를 받지 않도록, UI 연결 전 poll 처리부터 방어적으로 추가한다.

완료 기준:

- 차단된 pendingRef는 세션 발급 불가
- 원래 요청 브라우저는 차단 메시지 표시
- 감사 로그에 차단 이벤트 기록

## 8. 영향도 관리 방안

### 8.1 기존 소스코드 영향 최소화

| 대상 | 영향도 관리 |
| --- | --- |
| Backend 기존 승인 API | 응답 구조 변경 금지 |
| Backend Redis 기존 key | 삭제/이름 변경 금지 |
| UserFront 기존 QR 화면 | 삭제 금지, feature flag fallback 유지 |
| UserFront 기존 링크 화면 | QR 안정화 전까지 변경 금지 |
| OIDC Hydra/Kratos 연동 | 신규 metadata API는 Hydra/Kratos accept 흐름에 개입하지 않음 |

### 8.2 호환성 원칙

기존 클라이언트는 새 API를 몰라도 정상 동작해야 한다.

```text
기존 UserFront -> 기존 API만 사용 -> 정상
신규 UserFront flag OFF -> 기존 화면 사용 -> 정상
신규 UserFront flag ON -> approval/info 조회 후 기존 approve API 사용 -> 정상
```

### 8.3 실패 시 fallback

`approval/info` 조회가 실패하면 아래 정책 중 하나를 선택한다.

권장 기본값:

```text
운영/스테이징: 승인 차단 후 "요청 정보를 확인할 수 없습니다" 표시
개발 로컬: 기존 ApproveQrScreen fallback 허용
```

로컬 개발 중에는 빠른 검증을 위해 fallback을 허용하되, 운영에서는 요청 정보를 확인하지 못한 승인은 막는 쪽이 안전하다.

## 9. 개발 후 영향도 확인 체크리스트

### 9.1 Backend 테스트

필수 확인:

1. `POST /api/v1/auth/approval/info` 정상 응답
2. 없는 pendingRef는 안전한 오류 반환
3. QR init 후 approval info 조회 가능
4. QR approve 기존 테스트 통과
5. Enchanted Link init/poll 기존 테스트 통과
6. Magic Link verify 기존 테스트 통과
7. Login Code verify 기존 테스트 통과
8. Password login 기존 테스트 통과

권장 테스트 파일:

```text
backend/internal/handler/auth_handler_qr_test.go
backend/internal/handler/auth_handler_link_test.go
backend/internal/handler/auth_handler_login_code_test.go
backend/internal/handler/auth_handler_login_test.go
```

### 9.2 UserFront 테스트

필수 확인:

1. feature flag OFF에서 기존 QR 승인 화면이 표시된다.
2. feature flag OFF에서 QR 자동 승인이 기존처럼 동작한다.
3. feature flag ON에서 공통 승인 화면이 표시된다.
4. `내 요청입니다` 클릭 시 QR 승인 성공
5. 미로그인 승인 기기는 로그인 화면으로 이동
6. approval info 조회 실패 시 안전 안내 표시
7. 기존 로그인 링크 탭 동작 유지
8. 기존 비밀번호 로그인 동작 유지

권장 테스트 파일:

```text
userfront/test/qr_scan_screen_test.dart
userfront/test/login_navigation_race_test.dart
userfront/test/auth_proxy_service_test.dart
```

### 9.3 E2E 테스트

로컬에서 최소 2개 브라우저 또는 context로 확인한다.

시나리오:

1. PC 브라우저에서 QR 로그인 시작
2. 모바일 역할 브라우저에서 QR approve URL 접근
3. 공통 승인 화면에서 요청 정보 확인
4. `내 요청입니다` 클릭
5. PC 브라우저 poll이 성공하고 dashboard 진입

회귀 확인:

1. 비밀번호 로그인
2. 링크 로그인
3. QR 로그인 flag OFF
4. QR 로그인 flag ON
5. OIDC RP callback

## 10. 보안 확인 항목

개발 완료 전 반드시 확인한다.

| 항목 | 기준 |
| --- | --- |
| IP 노출 | 전체 IP가 아니라 마스킹된 값만 사용자에게 표시 |
| User-Agent | 원문 저장은 가능하나 UI에는 요약 표시 권장 |
| pendingRef 오류 | 사용자 존재 여부를 추론할 수 없는 메시지 사용 |
| approval/info | 인증 없이 호출 가능한 경우 rate limit 필요 |
| QR 승인 | 승인 기기의 기존 세션 검증 유지 |
| 링크 승인 | verifyOnly 흐름 유지 |
| 감사 로그 | 승인/차단/실패 이벤트가 구분되어야 함 |

## 11. 롤백 방안

문제가 발생하면 아래 순서로 롤백한다.

1. UserFront feature flag를 OFF로 변경한다.
2. 기존 `ApproveQrScreen` 경로로 즉시 복귀한다.
3. Backend 신규 API는 남겨도 기존 흐름에 영향이 없으므로 즉시 제거하지 않아도 된다.
4. 신규 Redis key는 TTL 만료로 자연 정리된다.
5. 문제가 Backend route 추가와 관련 있으면 `/approval/info`, `/approval/block` route만 비활성화한다.

롤백 시 제거하지 않아야 할 것:

- 기존 QR/link Redis key
- 기존 `/qr/approve`
- 기존 `/magic-link/verify`
- 기존 `/login/code/verify`
- 기존 `ApproveQrScreen`

## 12. 권장 구현 범위

1차 로컬 구현 범위는 아래로 제한하는 것이 좋다.

1. Backend `approval/info` API 추가
2. QR init에 공통 metadata 병행 저장
3. UserFront 공통 승인 화면 추가
4. QR 승인 라우트만 feature flag로 공통 화면 연결
5. 기존 QR 자동 승인 fallback 유지
6. 테스트로 flag OFF/ON 영향도 확인

2차 구현 범위:

1. Enchanted Link/Magic Link 화면 연결
2. `approval/block` API 추가
3. poll 응답 `blocked_by_user` 지원
4. 감사 로그 차단 이벤트 추가

## 13. 최종 완료 기준

이 작업은 다음 조건을 모두 만족해야 완료로 본다.

1. 기존 비밀번호 로그인에 영향이 없다.
2. 기존 링크 로그인에 영향이 없다.
3. 기존 QR 자동 승인 흐름은 feature flag OFF에서 그대로 동작한다.
4. feature flag ON에서 QR 공통 승인 화면이 표시된다.
5. 승인 화면에서 요청 기기/IP/시간/서비스가 표시된다.
6. `내 요청입니다` 승인 후 원래 요청 브라우저가 로그인된다.
7. approval info 조회 실패 시 세션 발급으로 이어지지 않는다.
8. Backend 단위 테스트와 UserFront 테스트가 통과한다.
9. 신규 Redis key는 TTL을 가진다.
10. 롤백 시 feature flag OFF만으로 기존 동작으로 돌아갈 수 있다.

