테마
Anonymous Grants
logi 의 익명-우선 가입 흐름(v0.4) 은 사용자가 이메일이나 SSO 를 거치기 전에도 logi 앱을 사용할 수 있게 해줍니다. 다만 외부 RP 가 익명 user 에게 grant 를 발급받는 것은 기본적으로 차단 됩니다 — RP 는 logi 로부터 "검증된 subject" 를 받는다는 전제로 통합하기 때문입니다.
allow_anonymous_grants 는 RP 가 이 전제를 의식적으로 완화하기로 선택하는 opt-in 플래그입니다.
언제 켜는가
전형적 사용처:
- Guest 모드 UX — 회원가입 전에 데모/체험을 제공하는 서비스. 익명 user 의 작업을 RP 가 일단 저장하고, 나중에 사용자가 SSO 로 promotion 했을 때 그 데이터를 연결.
- 수집-우선 RP — RP 가 자체적으로 phone/email 같은 추가 식별 정보를 받아 subject 를 enrich 하는 경우. logi 는 "이 사람이 누구인지" 모르고 단지 "어떤 device 에서 왔는지" 만 보장.
- B2B SDK / 임베드 — host app 이 자기 user 컨텍스트를 갖고 있고 logi 는 보조 서명만 제공하는 케이스.
켜지 않는 경우 (대부분의 일반 RP):
- 결제, 의료, 금융, 또는 법적 식별이 필요한 서비스.
- RP 가 logi sub 외에 추가 식별을 안 받는 단순 SSO 통합.
설정
콘솔의 RP 설정 페이지 → "고급" → "Allow anonymous grants" 토글. Admin API 로도 변경 가능합니다 — 단 일반 PATCH 가 아닌 전용 member action 을 사용해야 합니다 (admin 컨트롤러는 화이트리스트된 액션만 허용):
POST /api/v1/admin/applications/:id/edit_fields
{ "application": { "allow_anonymous_grants": true } }/admin/oauth_applications/... 같은 prefix 는 존재하지 않습니다 (전부 /admin/applications/...). 인증은 admin JWT + step-up 입니다.
기본값은 false. 켜는 즉시 변경되며 기존 grant 에는 영향을 주지 않습니다.
익명 user 가 차단될 때의 응답
RP 가 allow_anonymous_grants=false (기본값) 상태에서 익명 user 가 consent 를 시도하면 logi 는 HTTP 403 으로 다음 body 를 돌려줍니다:
json
{
"error": "anonymous_not_allowed",
"error_description": "Tennis Bracket 은 식별된 logi 계정만 허용해요. 설정 → 계정 → Apple/Google 연결 또는 이메일 등록 후 다시 시도해주세요.",
"requires_developer": false,
"self_rp": false,
"application_name": "Tennis Bracket",
"remediation": {
"action": "link_identity",
"user_facing_label": "계정 설정 열기"
}
}각 필드의 용도:
| 필드 | 용도 |
|---|---|
error_description | 사용자에게 보여줄 수 있는 한국어 안내. RP 이름이 박혀있음 — 차단 주체가 logi 가 아니라 해당 RP 의 정책임을 명시. |
application_name | RP 이름만 단독으로 필요할 때 (예: 커스텀 카피 조합). |
requires_developer | self-RP (1pass 콘솔) 에서 console:manage 같은 developer 전용 scope 요청 시 true. 그 외엔 false. |
remediation.action | link_identity (Apple/Google/email 추가) 또는 promote_to_developer (개발자 모드 승급). 모바일 sheet 가 분기 처리. |
remediation.user_facing_label | "계정 설정 열기" CTA 버튼 라벨. |
모바일/네이티브 RP 개발자
사용자에게 메시지를 그대로 노출하지 말고, application_name + remediation.action 을 조합해 자기 앱의 톤에 맞춰 다시 쓰는 것을 권장합니다. logi 가 돌려주는 한국어 카피는 안전한 fallback 입니다.
QR 로그인 (모바일 → 데스크톱) 도 동일 응답
데스크톱 브라우저가 띄운 QR 을 logi 앱이 스캔하고 POST /api/v1/oauth/qr/:id/approve 를 호출할 때, 익명 user 면 위와 동일한 anonymous_not_allowed shape 으로 403 을 돌려줍니다 (application_name + remediation). 단 QR flow 는 promotion.resume_token 을 포함하지 않습니다 — QR session 자체가 resume vehicle 이므로, 사용자가 promotion 을 끝낸 뒤 같은 session_uuid 로 /approve 를 한 번 더 호출하면 그대로 이어집니다.
Just-In-Time (JIT) Promotion
403 응답에는 promotion 객체 가 함께 옵니다. 이 객체로 logi 앱(또는 logi SDK 를 임베드한 RP) 이 사용자를 OAuth dance 밖으로 던지지 않고 인라인으로 Apple/Google/email 등록을 받아서 같은 흐름을 이어갈 수 있습니다. RP 입장에서는 차이 없음 — 평소처럼 ?code=&state= 콜백을 받습니다.
응답에 추가되는 promotion 객체
json
{
"error": "anonymous_not_allowed",
"error_description": "...",
"application_name": "Tennis Bracket",
"requires_developer": false,
"self_rp": false,
"remediation": { "action": "link_identity", "user_facing_label": "계정 설정 열기" },
"promotion": {
"required": true,
"reason": "identified_account",
"methods": [
{ "kind": "apple", "label": "Apple 로 가입", "start_url": "/api/v1/me/connected_identities" },
{ "kind": "google", "label": "Google 로 가입", "start_url": "/api/v1/me/connected_identities" },
{ "kind": "email_password", "label": "이메일·비밀번호로 가입", "start_url": "/api/v1/me/emails" }
],
"resume_token": "eyJhbGciOiJIUzI1NiJ9...",
"resume_endpoint": "/api/v1/oauth/authorize/resume",
"resume_expires_in": 300
}
}| 필드 | 의미 |
|---|---|
promotion.reason | identified_account (식별 계정만 받음) 또는 developer_role (개발자 모드 필요 — self-RP console:manage 시) |
promotion.methods[] | 모바일 sheet 가 렌더할 등록 옵션. kind 로 분기. developer_role 일 때는 email_password 만 옴 (개발자 모드는 검증된 이메일이 필요) |
promotion.resume_token | logi 가 사인한 single-use, 5분 TTL JWT. 원래의 client_id / redirect_uri / state / code_challenge / scope 를 모두 묶음 |
promotion.resume_endpoint | 등록 완료 후 POST 할 경로 (/api/v1/oauth/authorize/resume) |
promotion.resume_expires_in | 초 단위 (300 = 5분). 안에 못 끝내면 사용자가 다시 시작해야 함 |
흐름 (시퀀스)
mermaid
sequenceDiagram
participant App as RP 앱
participant Logi as logi (IdP)
participant Apple as Apple SDK
App->>Logi: POST /api/v1/oauth/authorize (anonymous PAK)
Logi-->>App: 403 + promotion { resume_token, methods }
Note over App: 인라인 promotion sheet 노출
App->>Apple: id_token 요청
Apple-->>App: id_token
App->>Logi: POST /api/v1/me/connected_identities<br/>{ provider:"apple", identity_token, raw_nonce }
Logi-->>App: 201 { connected_identities: [...] } (user now identified)
App->>Logi: POST /api/v1/oauth/authorize/resume<br/>{ resume_token }
Logi-->>App: 201 { code, state, redirect_uri }
App->>RP: open(redirect_uri?code=&state=)POST /api/v1/oauth/authorize/resume
Auth: Bearer PAK (resume_token 발급받은 같은 사용자).
Body: { "resume_token": "<JWT>" }
응답:
| Status | Body | 의미 |
|---|---|---|
| 201 | { code, state, redirect_uri } | 성공 — RP 콜백으로 코드 전달 가능 |
| 400 | { error: "invalid_request" } | resume_token 누락 |
| 401 | { error: "unauthenticated" } | PAK 없음 |
| 403 | { error: "resume_user_mismatch" } | resume_token 의 user ≠ 현재 PAK 의 user. 계정이 바뀌었음 — 처음부터 다시 |
| 403 | { error: "developer_required" } | 등록은 됐지만 개발자 모드까지 필요한 scope 였음 |
| 422 | { error: "promotion_incomplete" } | 토큰은 멀쩡하지만 user.anonymous 가 여전히 true. 등록 단계가 끝나지 않았음 |
| 422 | { error: "resume_token_expired" } | 5분 초과 |
| 422 | { error: "resume_token_already_used" } | 한 번 redeem 했음 |
| 422 | { error: "invalid_resume_token" } | 서명 불일치 / 형식 깨짐 / 만료 외 사유 |
보안 모델
- 단일 사용:
resume_token은 한 번 redeem 되면jti가 cache 에 마킹되어 재사용 차단. 크래시 로그/스크린샷에 노출되어도 두 번 못 씀. - 사용자 바인딩: 토큰 발급 시점의
user.id가 페이로드에 박힘. 등록 중에 다른 계정으로 PAK 가 갈리면resume_user_mismatch로 fail-closed. - 서버 검증 파라미터만 재생: client 가 보내는 raw body 가 아니라 토큰 안의 canonical (logi 가 직접 parse 해서 박은) client_id / redirect_uri / state / scope / code_challenge 로 재생.
code_challenge변조로 grant 가로채기 불가능. - TTL 5분: 등록 흐름이 사실상 1분 안에 끝나는 것을 가정. 더 길면 사용자가 deep-link 헷갈려서 그냥 다시 시도하는 게 안전.
RP 입장에서는?
아무것도 바꿀 필요 없음. 평소처럼 /oauth/authorize → ?code=&state= 콜백 → /oauth/token 교환만 하면 됩니다. logi 가 익명 사용자의 promotion 을 OAuth dance 안에서 흡수해서 RP 에게는 항상 식별된 user 로 코드를 넘깁니다.
JIT promotion 후 발급되는 grant 의 audit log 에는 via: "jit_promotion_resume" 가 기록되어 운영자가 "어떻게 들어온 consent 인지" 추적할 수 있습니다.
익명 user 의 grant 가 어떻게 다른가
allow_anonymous_grants=true RP 가 익명 user 의 grant 를 받으면 userinfo 응답은:
json
{
"sub": "9182",
"canonical_sub": "9182",
"is_canonical": true,
"anonymous": true,
"email": "anon+abc123@1pass.internal",
"email_verified": false,
"linked_subs": []
}핵심 차이:
anonymous: true— RP 가 이 user 가 익명 임을 인식할 수 있음.email은 internal placeholder (anon+<hash>@1pass.internal) — 실제 이메일 아님.email_verified: false.
RP 는 이 user 를 자기 도메인 모델에 저장할 때 "익명 placeholder" 표기를 해두는 것이 안전합니다.
익명 → identified promotion 시 동작
익명 user 가 나중에 Apple/Google SSO 로 promotion 되면 logi 가 자동으로:
- 같은 user.id 에
apple_sub또는google_sub채움. anonymous: false로 전환.- provider 가 이메일을 제공했으면
email_address를 실제 이메일로 교체. previously_anonymous: trueclaim 이 user 의 lifetime 내내 박힘.- 다음 토큰 회전 시 RP 는 변경된 claim 을 받음.
이때 RP-side 에서 해야 할 일:
- userinfo 의
anonymous가 true → false 로 바뀐 것을 감지해서 자기 user row 를 "promoted" 로 마킹. - 익명 시절에 모은 데이터를 promoted user 의 view 에 그대로 유지.
webhook 으로 별도 이벤트가 emit 되지는 않습니다 — promotion 은 같은 user.id 의 단순 update 이고 통합이 아니기 때문입니다. RP 는 token 회전 시점에 인지해야 합니다.
익명 user 의 통합
익명 user 가 다른 user 에게 흡수되거나 다른 user 를 흡수하는 시나리오:
- 익명 user 가 SSO promotion 도중 같은 이메일의 기존 user 와 매칭 → T2 트리거. 익명 user 가 흡수되어 사라지고 survivor 의 데이터로 통합. RP 는
user.merged받음. - 사용자가 T3 로 익명 계정을 명시적으로 흡수 → 가능하지만 흔하지 않음. dual PoP 만 통과하면 동작.
흡수된 익명 user 의 데이터는 linked_user_id 로 남아 RP-side logi_identity_links 에 기록됩니다.
RP 운영 체크리스트 (권장)
allow_anonymous_grants=true 를 켠 RP 가 사용자 신뢰를 유지하기 위해 점검하면 좋은 항목들입니다. 법적 요구사항(개인정보처리방침 등) 은 각 RP 의 관할 법령에 따라 별도 검토가 필요합니다.
- [ ] 익명 user 의 데이터를 별도 lifecycle 로 관리 (보존 기간, 익명 placeholder 정리 정책 등).
- [ ]
anonymous: true인 user 에게는 결제·법적 책임이 따르는 작업을 노출하지 않거나 제한하는 편이 안전합니다. - [ ] promotion 시점 (
anonymous: false로 전환) 에 익명 시절 데이터가 자연스럽게 연결되도록 설계. - [ ]
user.merged수신 시 익명 user 의 데이터도 canonical 로 정상 합쳐지는지 사전 테스트. - [ ] 개인정보처리방침에 "익명 사용 + 후속 promotion 시 데이터 연결" 정책을 명시 (관할 법령 검토 권장).
보안 고려
- 익명 grant 의 본인 확인 수단은 디바이스의
device_secret하나입니다. 결과적으로 디바이스 보안 수준이 곧 grant 의 보안 수준이 되므로, 높은 보안 수준이 요구되는 작업에는 익명 grant 를 사용하지 않기를 권장합니다. - 익명 user 가 30일 유예 기간 내에 SSO 를 연결하지 않으면
PurgeUserJob으로 hard-delete 됩니다. 이때 RP 는user.grants_revoked이벤트를 받습니다. 사용자가 grace 기간 안에 복구하려면POST /api/v1/account_recoveries흐름을 사용합니다. - 운영자는
oauth_applications.allow_anonymous_grants가 true 인 RP 목록을 주기적으로 점검하기를 권장합니다.