QR 로그인 (앱 푸시 승인)
WhatsApp Web · Slack · Discord 와 동일한 패턴. 사용자가 웹사이트에서 QR 코드를 보고, 모바일 logi 앱으로 스캔해 본인 확인 후 승인하면 브라우저가 자동으로 로그인됩니다.
RP(제휴사)에게 미치는 영향
없습니다. QR 로그인은 logi 의 /oauth/authorize 페이지가 사용자에게 제공하는 로그인 옵션 중 하나입니다. RP 가 받는 응답은 표준 Authorization Code Flow 와 동일한 redirect_uri?code=&state= 입니다. 별도의 SDK 변경, 클라이언트 설정, 또는 redirect_uri 추가가 필요하지 않습니다.
언제 활성화되나
logi 의 로그인 페이지(/session/new)와 OAuth Consent 화면(/oauth/authorize)에 "📱 QR 로 로그인" 버튼이 노출됩니다. 사용자가 직접 트리거할 때만 QR 흐름이 시작됩니다 (자동 redirect 안 함).
iOS Universal Link 가 server-side redirect chain 에서 트리거되지 않는 보안 정책 때문에, 표준 web→app 자동 점프는 안전하지 않거나 작동하지 않습니다. QR 은 사용자가 명시적으로 시작하는 out-of-band 채널을 제공해 이 한계를 우회합니다.
시퀀스 다이어그램
sequenceDiagram
autonumber
participant C as 제휴사 앱
participant B as 브라우저
participant L as logi 서버
participant A as logi 모바일 앱
C->>B: GET /oauth/authorize?client_id&redirect_uri&state&code_challenge&scope
B->>L: 같은 요청 도착
L-->>B: Consent 페이지 (또는 /session/new)
B->>L: "QR 로 로그인" 클릭 → POST /oauth/qr/start (OAuth params)
L->>L: QrLoginSession 생성 + signed cookie (uuid + browser_nonce)
L-->>B: { session_id, qr_payload, cable_path }
B->>B: QR 캔버스 렌더 + ActionCable 구독 (/cable, params: { session_id })
A->>A: 카메라로 QR 스캔 → { session: uuid, domain: "api.1pass.dev" }
A->>L: GET /api/v1/oauth/qr/:id (Bearer PAK)
L-->>A: { application, scopes, browser: { label, ip } }
A->>L: POST /api/v1/oauth/qr/:id/scan
L-->>B: Cable push { status: "scanned" }
A-->>A: OAuthConsentView 표시 (RP 이름·scope·브라우저)
A->>L: POST /api/v1/oauth/qr/:id/approve (Bearer PAK)
L->>L: session.transition!(scanned → approved, approved_user)
L-->>B: Cable push { status: "approved" }
B->>L: GET /oauth/qr/:id/complete (signed cookie 검증)
L->>L: transaction { transition!(approved → completed); OauthAccessGrant.create!; Consent.grant!; AuditLogger; LoginLog }
L-->>B: 302 redirect_uri?code=&state=
B->>C: redirect 도착 — 표준 OAuth 흐름 재진입
C->>L: POST /oauth/token (code_verifier)
L-->>C: access_token + refresh_token + id_token?보안 모델
| 위협 | 방어 |
|---|---|
| QR 도용 / 캡쳐 | session_uuid 단일 사용 (completed 후 재사용 불가) + TTL 10분 + sweeper job 으로 만료 처리 |
브라우저 탈취 (다른 기기에서 /complete 호출) | signed cookie 두 개 검증 — qr_session_uuid + qr_browser_nonce (32B 랜덤). UA/IP 가 아니라 secret nonce 로 binding |
| 앱 사칭 (PAK 없이 승인) | POST /approve 는 Bearer PAK 필수, 익명 user 거절 |
상태 race (동시 /approve × 2) | WHERE status = 'scanned' UPDATE status = 'approved' 원자적 전이. affected_rows=0 → 409 |
| 앱이 RP 사칭 | QR payload 의 domain 을 앱이 hard-code 검증 (api.1pass.dev 외 거절) |
| /complete 재호출로 grant 중복 | transaction 내부 transition!(approved → completed) 가 한 번만 성공 |
| CSRF (POST /oauth/qr/start) | Rails CSRF 토큰 (X-CSRF-Token 헤더) |
| Brute force / DoS | Rack Attack — start 10/min/IP, status 60/min/IP |
v0.x 에서 미적용 (v2.x roadmap)
- mTLS / DPoP — 앱-서버 mutual cert binding
- 서명된 QR payload (앱이 logi 의 ECDSA 서명을 검증)
- Suspicious-login detection (geolocation 변화, impossible travel)
만료 / 거절 / 에러
| 상황 | RP 가 받는 응답 |
|---|---|
| 사용자가 앱에서 "거절" | 302 redirect_uri?error=access_denied&state=... (표준) |
| QR 만료 (10분) | 302 redirect_uri?error=access_denied&state=... (표준) |
| browser_nonce cookie 변조 | 400 Bad Request (RP redirect 안 됨, 사용자가 다시 로그인) |
| 같은 session 재사용 시도 | 302 redirect_uri?error=access_denied&state=... |
access_denied 는 RFC 6749 §4.1.2.1 표준 코드입니다. RP 의 OAuth 라이브러리가 처리하는 그대로입니다.
RP 가 추가로 할 일 — 없음
위 시퀀스 다이어그램의 4–13번 단계는 모두 logi 의 책임입니다. RP 입장에서는 표준 Authorization Code + PKCE 와 1바이트도 다르지 않습니다.
// 기존 OAuth 코드 그대로
window.location.href = `https://api.1pass.dev/oauth/authorize?${qs}`;
// ...
const { code, state } = await receiveRedirect();
const { access_token } = await fetch("/oauth/token", { /* ... */ });사용자가 QR 로 로그인했는지 패스워드로 로그인했는지 RP 는 알 필요가 없고, JWT access_token 의 페이로드도 동일합니다.
모바일 앱 SDK API (logi 1pass 앱 전용)
QR 측의 4개 endpoint 는 logi iOS/Android 앱이 사용합니다. 외부 RP 에는 노출되지 않습니다 (PAK Bearer 인증 + 앱 등록 필요).
자세한 스펙은 API 레퍼런스 — Internal QR endpoints 의 /api/v1/oauth/qr/* 섹션을 참고하세요.