핵심 개념
역할
| OAuth 2.0 | logi 이름 | 역할 |
|---|---|---|
| Identity Provider | logi | 인증, 토큰 발급 |
| Relying Party | 제휴사 앱 | redirect, userinfo 조회 |
| Resource Owner | 사용자 | 자격 증명 입력, Consent |
users.role: user | developer | admin.
식별자
User
| 필드 | 설명 |
|---|---|
apple_sub / google_sub | SSO provider stable ID. 1순위 lookup. |
email_address | NOT NULL, active user 사이 unique (partial index deleted_at IS NULL). 익명: anon+<sha256(platform:device_uuid)[0..16]>@1pass.internal. SSO 시 provider 이메일이 없으면 apple+<hash>@1pass.internal 또는 google+<hash>@1pass.internal 합성. |
device_uuid | DeviceCredential row (user_id, device_uuid, platform). 멀티 디바이스용, user 식별자 아님. |
Client (RP)
| 필드 | 형식 | 비고 |
|---|---|---|
client_id | logi_ + 32 hex | 공개 |
client_secret | logi_secret_ + 64 hex | 1회 노출, DB는 bcrypt digest |
Token / Key
jti— Access Token JWT 고유 ID, revoke 조회용.kid— 서명 키 식별자, JWKS rotation 지원.
Apple Bundle ID
| 플랫폼 | Bundle ID | TeamID |
|---|---|---|
| iOS | com.dcodelabs.logi | 74PTNNLD4P |
| App Clip | com.dcodelabs.logi.Clip | 74PTNNLD4P |
| macOS | com.dcodelabs.logi.mac | 74PTNNLD4P |
AASA appID 와 일치해야 함.
익명-우선 (v0.4)
| 단계 | user 상태 | 식별자 |
|---|---|---|
| 첫 실행 | anonymous=true, sub=nil, email_address=anon+<hash>@1pass.internal, password=random | DeviceCredential |
| 첫 SSO | anonymous=false, sub 채움, email 교체 | provider sub |
| 30일 grace 재로그인 | find_restorable_within_grace 로 deleted user 복구 (account-deletion) | provider sub / email |
⚠️ Cross-provider 충돌 (비대칭):
- Apple 경로: 동일 이메일 Google-only user 에 apple_sub link. 다른 apple_sub 보유 시
email_linked_to_other_apple_account거부. - Google 경로: 사전 검사 없이
User.create!→ActiveRecord::RecordNotUnique가능 (임시).
익명 user OAuth 동의
기본 거부. 예외:
- self-RP (1pass console):
console:read익명 허용,console:manage는 developer 필요. - per-RP opt-in:
oauth_applications.allow_anonymous_grants=trueRP 는 익명 grant 수락.
Scope
| scope | userinfo 반환 키 |
|---|---|
profile:basic | sub, nickname, name (profile alias 허용, profile:basic 권장) |
email | sub, email, email_verified |
phone | sub, phone_number |
openid | id_token 발급 + sub (OIDC 활성화) |
공백 구분 (콤마 아님). 전체 목록·필드는 Scope 레퍼런스.
신원은 userinfo 에서
email·nickname·name 등 프로필 claim 은 /oauth/userinfo 응답에 담깁니다. id_token 은 sub 등 OIDC 표준 claim 만 담으므로, 프로필이 필요하면 access_token 으로 userinfo 를 호출하세요.
Consent 재사용
- 동일 scope 재인증 → UI 스킵, 즉시 code 발급
- scope 확장 → "NEW" 배지 + 추가 동의
/settingsrevoke → 다음 인증 시 Consent 재표시
토큰 수명
| 토큰 | 만료 | Revocation |
|---|---|---|
| Authorization Code | 10분, 1회 | 자동 (consume!) |
| Access Token (JWT) | 15분 | jwt_jti DB 조회 |
| Refresh Token | 30일, rotation | 재사용 시 체인 전체 revoke |
| Personal API Key | 무기한 (설정 가능) | last_used_at + 수동 revoke |
| Soft-deleted user grace | 30일 | SSO/email 재로그인 자동 복구, 30일 후 PurgeUserJob (상세) |
JWT 구조
Header: { alg: "RS256", kid: "<active>", typ: "JWT" }
Payload: { iss: "logi", sub: "<user_id>", aud: "<client_id>",
exp: <15min>, iat: <now>, jti: "<uuid>", scope: "openid profile:basic email" }
Signature: RS256 over header.payload공개 키: /.well-known/jwks.json
인증 메커니즘
| 메커니즘 | 용도 | 전달 |
|---|---|---|
| 세션 쿠키 | 웹 UI | session_id=...; Secure; HttpOnly; SameSite=Lax |
| OAuth AT (JWT) | userinfo 조회 | Authorization: Bearer <JWT> |
| PAK | CLI/MCP | Authorization: Bearer logi_pak_... |
| Client Basic | /oauth/token | Authorization: Basic <client_id:secret> |
| Passkey (WebAuthn) | 패스워드리스 | ASAuthorization* / navigator.credentials |
| device_secret | 익명 1차 자격증명 (POST /api/v1/devices 1회 발급, keychain) | body { "device_secret": "<urlsafe_base64>" } (prefix 없음) |
2FA 상태 머신
[비활성] --setup_otp!--> [키 생성됨] --enable_otp!(code)--> [활성]
[활성] --disable_otp!(current_code)--> [비활성]
[활성] --login_with(otp_code)--> session.otp_verified_at = Time.current
[활성] --login_with(backup_code)--> 백업 1개 소진Passkey + User Verification → OTP 동등, otp_verified_at 자동 설정.
Sub Stability & canonical_sub
sub— grant 시점 user.id 영구 박힘. OIDC stable.canonical_sub— 현재 살아있는 user.id. 통합 시 갱신. RP 가 토큰 검증 시 참조.linked_subs— 흡수된 user.id 목록. survivor userinfo 에 포함.
상세: Sub 정책, RP Migration Guide.
Account Linking (identity_links)
primary_user_id (survivor canonical)
linked_user_id (흡수, UNIQUE)
merged_via ("t1_device_link" | "t2_email_match" | "t3_otp")DB 트리거가 chain/cycle 차단 → canonical 해석 1-hop. T1/T2/T3 동작: Account Merge.