Files
baron-safe-app/docs/nonstandard-login-common-approval-screen-plan-2026-06-23.md
T
kevin 684cf87702
ci / flutter-check (push) Successful in 4s
Add Baron Safe planning documents
2026-06-30 15:31:51 +09:00

21 KiB

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

작성일: 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. 목표 화면

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

새 로그인 요청

요청 서비스: 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 응답 모델을 추가한다.

권장 위치:

backend/internal/domain/auth_models.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를 추가한다.

POST /api/v1/auth/approval/info

요청:

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

응답:

{
  "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"
}

구현 위치:

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

route 추가:

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 저장 함수를 추가한다.

권장 함수:

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

권장 Redis key:

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가 필요하다.

POST /api/v1/auth/approval/block

요청:

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

응답:

{
  "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 공통 모델 추가

권장 파일:

userfront/lib/features/auth/domain/login_approval_request.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 추가

권장 파일:

userfront/lib/core/services/auth_proxy_service.dart

추가 함수:

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

요청 대상:

POST /api/v1/auth/approval/info

영향도:

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

6.3 공통 승인 화면 추가

권장 파일:

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

props:

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차 변경안:

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

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

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

권장 feature flag:

USERFRONT_COMMON_APPROVAL_SCREEN=true

또는 Flutter runtime env:

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

영향도 최소화:

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

링크와 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가 있는 로그인 정상

권장 명령:

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: 요청 정보 확인 후 수동 승인 성공
  • 미로그인 승인 기기는 기존처럼 로그인 유도

작업:

  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를 몰라도 정상 동작해야 한다.

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

8.3 실패 시 fallback

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

권장 기본값:

운영/스테이징: 승인 차단 후 "요청 정보를 확인할 수 없습니다" 표시
개발 로컬: 기존 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 기존 테스트 통과

권장 테스트 파일:

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. 기존 비밀번호 로그인 동작 유지

권장 테스트 파일:

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만으로 기존 동작으로 돌아갈 수 있다.