Demo 페이지 둘러보기
Source:
guide/demo-page-walkthrough.md· Live: https://docs.1pass.dev/guide/demo-page-walkthrough LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).
📍 Jump-to Index
- L34-L57: ## 시나리오 카탈로그
- L49-L57: ### 시나리오 구분 포인트
- L58-L116: ## 라우트 매핑
- L92-L103: ### 핵심 파일
- L104-L116: ### Back-compat 레거시 경로
- L117-L180: ## QR 시나리오 — multi-device 핸드오프
- L121-L157: ### 시퀀스
- L158-L164: ### 만료 / 보안
- L165-L180: ### 상태 상수
- L181-L188: ## Mac 시나리오
- L189-L200: ## Agent Approval 시나리오 — human-in-the-loop
- L201-L259: ## CSP 청정 — RP 통합 시 따라할 패턴
- L251-L259: ### RP 체크리스트
- L260-L281: ## 콜백 타임아웃
- L264-L273: ### 에러 분기
- L274-L281: ### Double-callback 안전성
- L282-L288: ## 다음 단계
Demo 페이지 둘러보기
demo.1pass.dev 는 1pass IdP 통합 모드 6 종을 직접 눌러볼 수 있는 walking sample. 외부 자료에서 시나리오별로 deep-link 하기 좋습니다. 호스트는 start.1pass.dev 세션 쿠키 누출을 막기 위해 분리되어 있습니다.
시나리오 카탈로그
| URL | 컨트롤러 | 대상 / 한 줄 설명 |
|---|---|---|
/ | Web::Demo::IndexController#show | 카탈로그 허브 (6 모드 비교) |
/oauth | Web::Demo::OauthController#show | 데스크톱 OAuth 2.0 + PKCE 표준 흐름 |
/qr | Web::Demo::QrController#show | 데스크톱 ↔ iPhone QR 핸드오프 (DemoPairingSession + ActionCable) |
/app-clip | Web::Demo::AppClipController#show | iOS App Clip (앱 미설치 사용자, .Clip 번들, AASA appclips.apps) |
/ios | Web::Demo::IosController#show | iOS 네이티브 (앱 설치 후, Universal Link + Swift SDK) |
/android | Web::Demo::AndroidController#show | Android (Intent setPackage first-try + Custom Tabs fallback) |
/mac | Web::Demo::MacController#show | macOS (.mac 번들, Universal Link + 키체인 PKCE) |
/agent-approval | Web::Demo::AgentApprovalController#show | Agent Approval Gate 시연 (Agent → 인증 요청 → 모바일 승인/거부) |
각 시나리오 페이지는 narrative + 기술 흐름 box + CTA 구조의 읽기 전용 랜딩. CTA 가 /demo/* 백엔드 액션을 호출해 실제 PKCE 라운드트립을 시작합니다.
시나리오 구분 포인트
/app-clipvs/ios: App Clip = 앱 미설치 사용자용 (QR/NFC 마케팅 면),/ios= 앱 설치 사용자 RP 내부 인증 (Swift SDK 만 붙이면 끝)./iosfirst-try 패턴:UIApplication.open(URL:, options: [.universalLinksOnly: true])→ false 반환 시 App Clip/Safari fallback./android:Intent(ACTION_VIEW, uri).setPackage("com.dcodelabs.logi")→ActivityNotFoundExceptioncatch →CustomTabsIntentfallback. iOS App Clip 같은 메커니즘 없음 → 미설치 시 Play Store 안내 또는 웹 Custom Tabs.- App Clip invocation URL:
api.1pass.dev/demo/clip(AASA 등록). 비 iOS 디바이스가 같은 QR 스캔 시demo.1pass.dev/로 301 redirect (dead link 방지).LOGI_API_HOSTenv 로 staging override 가능.
라우트 매핑
config/routes.rb 의 demo_host_constraint 블록:
constraints(demo_host_constraint) do
# Scenario landing pages
root to: "web/demo/index#show", as: :demo_host_root
get "oauth" => "web/demo/oauth#show", as: :demo_oauth
get "qr" => "web/demo/qr#show", as: :demo_qr
get "ios" => "web/demo/ios#show", as: :demo_ios
get "android" => "web/demo/android#show", as: :demo_android
get "app-clip" => "web/demo/app_clip#show", as: :demo_app_clip
get "mac" => "web/demo/mac#show", as: :demo_mac
# Agent Approval Gate demo (human-in-the-loop)
get "agent-approval" => "web/demo/agent_approval#show", as: :demo_agent_approval
post "agent-approval/send" => "web/demo/agent_approval#send_request", as: :demo_agent_approval_send
# OAuth back-end actions (PKCE). /demo 는 /oauth alias (App Clip QR back-compat)
get "demo" => "web/demo#show", as: :demo
get "demo/start" => "web/demo#start", as: :demo_start
get "demo/callback" => "web/demo#callback", as: :demo_callback
# Mac back-end
get "demo/mac/install" => "web/demo#mac_install", as: :demo_mac_install
post "demo/mac/notify" => "web/demo#mac_notify", as: :demo_mac_notify
# QR pairing back-end
get "demo/pair/:pair_id" => "web/demo_pair#show", as: :demo_pair
get "demo/pair/:pair_id/result" => "web/demo_pair#result", as: :demo_pair_result
end핵심 파일
app/controllers/web/demo/{index,oauth,qr,app_clip,ios,android,mac}_controller.rb— 시나리오 랜딩 (allow_unauthenticated_access,layout "demo")app/controllers/web/demo_controller.rb— back-compat/demo/*액션app/controllers/web/demo_pair_controller.rb— 데스크톱 측 페어링app/controllers/api/v1/demo_pair_controller.rb— iPhone 측 완료 엔드포인트app/views/layouts/demo.html.erb— importmap 없는 CSP-clean layoutapp/models/demo_pairing_session.rb— 페어링 상태 머신app/channels/demo_pairing_channel.rb— 데스크톱 ActionCable 구독app/assets/builds/demo-pairing.js— vanilla ActionCable 클라이언트e2e/scenarios/06-demo-pages.sh— 200 응답 검증
Back-compat 레거시 경로
| 경로 | 동작 |
|---|---|
/demo | /oauth alias |
/demo/start | PKCE state/verifier 생성 → api.1pass.dev/oauth/authorize 302 |
/demo/callback | code → /oauth/token 교환 → /oauth/userinfo |
/demo/mac/install · /demo/mac/notify (POST) | Mac 출시 알림 placeholder |
/demo/pair/:pair_id · /result | QR 핸드오프 iPhone 착륙 / 데스크톱 결과 |
/demo/clip | 레거시, demo.1pass.dev/ 301 (실제 invocation URL 은 api.1pass.dev/demo/clip) |
QR 시나리오 — multi-device 핸드오프
/qr 로드 → DemoPairingSession 생성 (WAITING), cookies.signed[:demo_pair_id] 발급, QR 렌더. iPhone 스캔 → /demo/pair/:pair_id 착륙 → OAuth 라운드트립 → 완료 시 ActionCable 로 데스크톱 자동 갱신.
시퀀스
Desktop (브라우저) Rails 서버 iPhone (logi 앱)
│ │ │
│ GET /qr │ │
│──────────────────────►│ │
│ │ DemoPairingSession.create! │
│ │ (state: WAITING) │
│ HTML + QR + cookie │ │
│◄──────────────────────│ │
│ subscribe Channel │ │
│ (pair_id) │ │
│──────────────────────►│ │
│ confirm_subscription │ │
│◄──────────────────────│ │
│ │ GET /demo/pair/:pair_id │
│ │◄─────────────────────────────│ (QR scan)
│ │ /oauth/authorize 라운드트립 │
│ │◄────────────────────────────►│
│ │ POST /api/v1/demo/pair/ │
│ │ :pair_id/complete │
│ │ (Bearer PAK + userinfo) │
│ │◄─────────────────────────────│
│ │ transition!(WAITING → │
│ │ COMPLETED) │
│ ActionCable.broadcast│ │
│ {state, result_path} │ │
│◄──────────────────────│ │
│ location.replace(...) │ │
│ GET /demo/pair/:id/ │ │
│ result │ │
│──────────────────────►│ │
│ 결과 화면 (userinfo) │ │
│◄──────────────────────│ │만료 / 보안
expires_at = 5.minutes.from_now.DemoPairingChannel#subscribed가expired?+terminal?둘 다 검사.cleanup_expired!가 비-terminal 만료 행을EXPIRED로 이동.cookies.signed[:demo_pair_id]가 채널pair_id와 일치해야 구독 허용 (제3자 엿듣기 차단).- 완료된 페어에 재-POST → atomic
transition!가드 + idempotent → 같은 200 응답, 재방송 없음. client_metadata(client_id / redirect_uri / scope) 가Web::DemoController#show와 정확히 일치해야 함. 불일치 시 iPhone 측 authorize URL 이 invalid_request 로 조용히 깨짐.
상태 상수
정상 경로: WAITING, SCANNED, APPROVED, COMPLETED
종료 상태: COMPLETED, DENIED, EXPIRED (TERMINAL_STATES)검증된 전이:
WAITING / SCANNED / APPROVED → COMPLETED—Api::V1::DemoPairController#completeatomictransition!WAITING / SCANNED / APPROVED → EXPIRED—cleanup_expired!DENIED전이는 정의되어 있으나 현재 트리거 경로 없음
TERMINAL_STATES 행은 전이/구독 모두 거부됩니다.
Mac 시나리오
Mac 앱 (com.dcodelabs.logi.mac build 24) entitlement: applinks:api.1pass.dev + applinks:open.1pass.dev. OAuth 흐름은 /session/new 의 "Mac 앱에서 열기" 카드가 담당. /mac 페이지 CTA 는 /demo/mac/install placeholder.
데모 intent handler 출시 후: trampoline route (출시 시점 확정) → 앱 설치 시 AASA 매칭 + 자체 PKCE / 미설치 시 /demo/mac/install 다운로드 fallback. OAuth authorize 직링은 PKCE 누락 fallback 이슈로 사용 안 함 (Universal Links 흔한 실수 참조).
Agent Approval 시나리오 — human-in-the-loop
/agent-approval 은 Agent Approval Gate walking sample 입니다. 외부 Agent (예: Codex, LLM 워크플로) 가 사용자 계정에서 민감한 액션 (광고 예산 변경, 결제, 권한 부여 등) 을 실행하려 할 때, IdP 가 사용자의 모바일 1pass 앱으로 push 를 보내 승인/거부 한 번 받고 결과를 Agent 에게 통보하는 흐름.
데모 페이지의 [Send sample approval] 버튼을 누르면 production 과 동일한 Agents::RequestCreator 파이프라인을 통해 AgentApprovalRequest 가 생성되고 (rate limit / audit / in-flight suppression 그대로), 페어링된 데모 사용자 폰으로 push 가 발사됩니다. 데모는 결정 결과를 브라우저에서 polling 하지 않습니다 — 실제 결정 조회 endpoint (/api/agents/approvals/:id) 는 agent 의 bearer + 서명을 요구하기 때문에 브라우저에 그 권한을 위임하지 않는 게 의도된 동작. 페이지는 "폰에서 푸시 확인 후 새로고침" 안내만 표시합니다. 라이브 polling 이 필요한 경우 RP 자체 백엔드가 agent 인증으로 polling 해 사용자에게 UI 갱신을 push 하는 패턴을 권장.
- 백엔드 컨트롤러:
app/controllers/web/demo/agent_approval_controller.rb(show+send_request) - idempotency: 방문자 IP + 분 단위 키 (
demo-#{ip}-#{minute}) 로 새로고침 루프가 agent quota 를 소진하지 못하게 차단. - seed 데이터: 데모 user / agent 가 별도 seed 로 주입되어야 하며, 부재 시 컨트롤러가 placeholder 를 렌더링.
CSP 청정 — RP 통합 시 따라할 패턴
데모 layout 은 importmap / Stimulus 를 빼고 정적 <script src> 하나만 로드 → script-src 'self' https: (unsafe-inline 없음) 환경에서 nonce 없이 동작.
<%# app/views/layouts/demo.html.erb (head 발췌) %>
<%= csp_meta_tag %>
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
<%= javascript_include_tag "demo-pairing", defer: true %>ActionCable 동작은 @rails/actioncable 없이 vanilla JS 약 100 줄:
// app/assets/builds/demo-pairing.js (subscribe 발췌)
var identifier = JSON.stringify({
channel: "DemoPairingChannel",
pair_id: pairId
});
function connect() {
ws = new WebSocket(cableUrl()); // wss:// 또는 ws://
ws.onopen = function () {
ws.send(JSON.stringify({ command: "subscribe", identifier: identifier }));
};
ws.onmessage = function (event) {
var data = JSON.parse(event.data);
if (data.type === "confirm_subscription") {
attempts = 0; // healthy → reset backoff
return;
}
if (data.identifier === identifier && data.message) {
if (data.message.state === "completed" && data.message.result_path) {
done = true;
ws.close();
window.location.replace(data.message.result_path);
}
}
};
ws.onclose = function () {
if (!done) scheduleReconnect(); // exponential backoff, max 10 retries
};
}RP 체크리스트
- inline
<script>금지 — 외부<script src>한 개로. - importmap 의존성 제거 —
<script type="importmap">도 inline 으로 차단됨. - DOM 데이터는
data-*속성으로 —data-demo-pairing-pair-id,data-demo-pairing-result-path를 ERB 에서 박고 JS 가getAttribute로 읽기. - WebSocket 직접 핸들링 —
wss://your-domain/cable에 붙으면 라이브러리 없이 ActionCable 채널 수신 가능.
콜백 타임아웃
/demo/start → /demo/callback 유효 시간: 30분 (첫 앱 설치 + Apple SSO + 인앱 escape + TestFlight 등 외부 동선 대응). PKCE state 는 1회용이라 공격면 증가 미미.
에러 분기
| 상태 | 표시 |
|---|---|
started_at 살아있고 30분 이내, state 일치 | 정상 토큰 교환 |
| 30분 초과 | "데모 세션이 만료되었습니다" |
| 세션 자체 없음 (다른 브라우저 콜백) | "데모 세션을 찾을 수 없습니다" |
| 세션 있고 state 불일치 | "state mismatch — 보안상의 이유로 인증을 중단했습니다" |
| 정상 처리 후 같은 콜백 URL 재클릭 | "데모 세션을 찾을 수 없습니다" (1회차에서 clear) |
Double-callback 안전성
앱 → Safari 핸드오프 / prefetch / 사용자가 새 탭으로 콜백 열기 시 동일 (state, code) 페어 콜백 중복 발생. 컨트롤러는 성공 또는 명확한 실패 후에만 session clear (demo_controller.rb#callback) — 첫 호출이 state 를 미리 소비해 두 번째가 mismatch 로 보이는 함정 회피.
회귀 spec: spec/requests/web/demo_spec.rb ("timeout boundary" / "double-call safety").
다음 단계
- Quickstart (5분) — curl 만으로 OAuth 라운드트립
- Universal Links 통합 가이드 — Apple OS 핸드오프 패턴
- OAuth Authorization Code Flow —
/oauth시나리오 표준 시퀀스 - 📱 모바일 트랙 · 🌐 웹 트랙