테마
Dual-Channel: 비밀번호 + 푸시 알림 병행 (Recommended)
Recommendation — 1pass 의
/session/new우측 비밀번호 컬럼은 "확인하기" 버튼 한 번으로 logi 앱에 푸시를 보내면서 동시에 비밀번호 입력으로도 로그인을 진행할 수 있는 dual-channel 패턴을 채택합니다. 같은 패턴을 자체 로그인 UI 를 구축하는 RP 에도 권장합니다.
왜 dual-channel 인가
단일 채널은 각자 실패 케이스를 안 막아 줍니다.
| 단일 채널 | 실패 시나리오 |
|---|---|
| 앱 Universal Link 만 | 카톡·네이버·페이스북 인앱 브라우저에서 redirect 차단 |
| 푸시 알림만 (1탭 승인) | 푸시 미수신 (배터리 절약 / Doze / 권한 거부 / 토큰 만료) |
| 비밀번호만 | 폰에 비밀번호 매니저 없을 때 모바일 타이핑 고통 + audit trail 없음 |
병렬로 두 경로를 살려두면 사용자는:
- 푸시가 오면 → 폰에서 [승인] 1 탭 → 자동 redirect
- 푸시가 늦거나 안 오면 → 그 자리에서 비밀번호 입력 → 정상 로그인
- 비밀번호 모르면 → 푸시 기다리거나 magic-link/패스키 등 다른 경로
먼저 끝나는 쪽이 우선. 둘 다 살아 있다는 것 자체가 보안 알림(audit) 효과도 줍니다 — 본인 아닌 시도면 푸시로 즉시 인지 가능.
UI 패턴
┌─ 이메일로 로그인 ──────────────────────────┐
│ │
│ 이메일 │
│ [you@example.com ] │
│ │
│ [📲 logi 앱으로 확인하기] │
│ 앱에 알림이 가요. 앱에서 승인하거나 │
│ 아래 비밀번호로 로그인할 수 있어요. │
│ │
│ 비밀번호 │
│ [•••••••• ] │
│ │
│ [로그인] │
│ │
│ 비밀번호를 잊으셨나요? │
└────────────────────────────────────────────┘핵심:
- 하나의 이메일 필드를 두 경로가 공유 — 사용자가 같은 값을 두 번 입력하지 않음
- "확인하기" 는 secondary button (text-xs / glass-button) — 비밀번호 + 로그인이 여전히 primary path
- 상태 표시는 inline — 폼 아래 작은 영역에 "✓ 알림 발송됨" / "✗ 발송 실패" 등
- 백그라운드 폴링 — 사용자가 비번 입력하는 동안에도
/oauth/push/:id/status폴링 유지. 승인 들어오면 자동 redirect
1pass 내부 구현 (app/views/sessions/new.html.erb)
erb
<div class="glass-card"
data-test-id="email-fallback-column"
<% if @qr_oauth_params.present? %>
data-controller="push-approval"
data-push-approval-start-url-value="<%= oauth_push_start_path %>"
data-push-approval-status-url-template-value="<%= oauth_push_status_path(':id') %>"
data-push-approval-complete-url-template-value="<%= oauth_push_complete_path(':id') %>"
data-push-approval-oauth-params-value="<%= @qr_oauth_params.to_json %>"
<% end %>>
<%= form_with url: session_url, local: true do |form| %>
<%= form.email_field :email_address,
data: { push_approval_target: "email" } %>
<% if @qr_oauth_params.present? %>
<button type="button"
data-action="click->push-approval#start"
data-push-approval-target="submit">
📲 logi 앱으로 확인하기
</button>
<div data-push-approval-target="statusBox" hidden></div>
<div data-push-approval-target="errorBox" hidden></div>
<% end %>
<%= form.password_field :password %>
<%= form.submit "로그인" %>
<% end %>
</div>요점:
data-controller="push-approval"가 RP 콘텍스트(@qr_oauth_params) 있을 때만 mount- 이메일 input 이 폼 안에 있지만
data-push-approval-target="email"로 Stimulus 와도 공유 - "확인하기" 는
type="button"이라 폼 submit 안 일어남 — Stimulus 만 트리거 - "로그인" submit 은 원래대로 평범한
POST /session— Stimulus 무관
자체 로그인 UI 를 구축하는 RP 가 따라야 할 패턴
RP 가 1pass /oauth/authorize 콘센트 화면을 거치지 않고 embedded auth 로 자체 로그인 폼을 만든다면:
- 이메일 필드 + secondary "확인하기 (선택)" 버튼
- 클릭 시 브라우저에서
POST /oauth/push/start호출 — body 에{ email, client_id, redirect_uri, code_challenge, code_challenge_method, state, scope }(OAuth authorize params 그대로 전달). 응답{ request_id, expires_in }. - 반환된
request_id로 클라이언트가GET /oauth/push/:id/status폴링 (1.5s 간격) approved응답 시 →window.location = /oauth/push/:id/complete(서버가 grant 발급 + RP redirect_uri 로 302 bounce). 모바일 측 승인은POST /api/v1/push_approvals/:id/approve(logi 앱이 호출, 외부 RP 무관).- 동시에 비밀번호/패스키 등 본인 인증 경로 살려둬서 사용자가 어느 쪽이든 먼저 끝낼 수 있게
Push 승인 흐름 자체의 전체 시퀀스 다이어그램은 Demo Page Walkthrough — Push Approval 참고. 서버측 endpoint 는 Oauth::PushApprovalsController (POST /oauth/push/start, GET /oauth/push/:id/status, GET /oauth/push/:id/complete) 와 모바일측 Api::V1::PushApprovalsController (POST /api/v1/push_approvals/:id/{approve,deny}).
보안 고려
| 항목 | 안전 보장 |
|---|---|
| 이메일 enumeration | /oauth/push/start 는 등록 안 된 이메일에도 200 반환 + synthetic request_id 발급. 폴링은 영원히 pending 유지. enumeration 차단. |
| 푸시 폭탄 (DoS) | PushApprovalRequest 에 cool-down (TTL 윈도우 내 같은 이메일 재시도 제한). 인입 단계에서 rate-limit. |
| MITM / 채널 혼선 | request_id 는 unguessable UUID. /oauth/push/:id/complete 는 발급된 client 의 cookie 와 OAuth state 둘 다 검증. |
| 본인 부재 시 비번 노출 | 푸시 알림 본문에 "비밀번호로 로그인 시도 중" 명시 + 30초 내 [차단] 탭 가능. 본인 아닌 시도 즉시 차단. |
안티패턴
- ❌ "확인하기" 가 비번 필드를 disable — 푸시 미수신 사용자가 빠져나갈 수 없음
- ❌ "확인하기" 가 푸시 발송 후 자동 reload — 백그라운드 폴링 잃음, 비번 입력 중인 사용자에게 disruptive
- ❌ "확인하기" 가 submit 버튼과 같은 위치/스타일 — 사용자가 헷갈림. secondary affordance 로 명확히 demote
- ❌ 이메일 필드 두 개 — 같은 값을 두 번 입력시키지 말 것
관련 문서
- Demo Page Walkthrough — 데모 RP 의 실제 흐름 + push approval sequence
- Public Clients — secret 없는 RP 의 자체 인증 패턴
- Post-login Destination — 로그인 완료 후 라우팅 규칙