Skip to content

Core concepts

Roles

OAuth 2.0logi termRole
Identity ProviderlogiAuthenticates, issues tokens
Relying Partypartner appRedirects, looks up userinfo
Resource OwneruserEnters credentials, gives consent

users.role: user | developer | admin.

Identifiers

User

FieldDescription
apple_sub / google_subSSO provider stable ID. The primary lookup.
email_addressNOT NULL, unique among active users (partial index deleted_at IS NULL). Anonymous: anon+<sha256(platform:device_uuid)[0..16]>@1pass.internal. On SSO, if the provider has no email, a synthetic apple+<hash>@1pass.internal or google+<hash>@1pass.internal is used.
device_uuidA DeviceCredential row (user_id, device_uuid, platform). For multi-device support — not a user identifier.

Client (RP)

FieldFormatNotes
client_idlogi_ + 32 hexPublic
client_secretlogi_secret_ + 64 hexShown once, stored as a bcrypt digest in the DB

Token / Key

  • jti — the Access Token JWT's unique ID, used for revocation lookups.
  • kid — the signing key identifier, supports JWKS rotation.

Apple Bundle ID

PlatformBundle IDTeamID
iOScom.dcodelabs.logi74PTNNLD4P
App Clipcom.dcodelabs.logi.Clip74PTNNLD4P
macOScom.dcodelabs.logi.mac74PTNNLD4P

Must match the AASA appID.

Anonymous-first (v0.4)

StageUser stateIdentifier
First launchanonymous=true, sub=nil, email_address=anon+<hash>@1pass.internal, password=randomDeviceCredential
First SSOanonymous=false, sub filled in, email swappedprovider sub
Re-login within the 30-day grace periodRestores a deleted user via find_restorable_within_grace (account-deletion)provider sub / email

⚠️ Cross-provider collision (asymmetric):

  • Apple path: links apple_sub to an existing Google-only user with the same email. If the user already has a different apple_sub, it is rejected with email_linked_to_other_apple_account.
  • Google path: no pre-check, so User.create! may raise ActiveRecord::RecordNotUnique (temporary).

Denied by default. Exceptions:

  • self-RP (1pass console): console:read is allowed for anonymous users; console:manage requires a developer.
  • per-RP opt-in: an RP with oauth_applications.allow_anonymous_grants=true accepts anonymous grants.

Scope

scopeuserinfo keys returned
profile:basicsub, nickname, name (profile alias allowed; profile:basic recommended)
emailsub, email, email_verified
phonesub, phone_number
openidissues an id_token + sub (enables OIDC)

Space-separated (not comma-separated). For the full list and fields, see the Scope reference.

Identity comes from userinfo

Profile claims like email, nickname, and name are carried in the /oauth/userinfo response. The id_token carries only standard OIDC claims like sub, so if you need the profile, call userinfo with the access_token.

  • Re-authenticating with the same scope → skips the UI, issues a code immediately
  • Scope expansion → "NEW" badge + additional consent
  • /settings revoke → the consent screen reappears on the next authentication

Token lifetimes

TokenExpiryRevocation
Authorization Code10 min, single-useAutomatic (consume!)
Access Token (JWT)15 minjwt_jti DB lookup
Refresh Token30 days, with rotationThe entire chain is revoked on reuse
Personal API KeyNo expiry (configurable)last_used_at + manual revoke
Soft-deleted user grace30 daysAuto-restored on SSO/email re-login; after 30 days, PurgeUserJob (details)

JWT structure

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

Public keys: /.well-known/jwks.json

Authentication mechanisms

MechanismUseTransport
Session cookieWeb UIsession_id=...; Secure; HttpOnly; SameSite=Lax
OAuth AT (JWT)userinfo lookupAuthorization: Bearer <JWT>
PAKCLI/MCPAuthorization: Bearer logi_pak_...
Client Basic/oauth/tokenAuthorization: Basic <client_id:secret>
Passkey (WebAuthn)PasswordlessASAuthorization* / navigator.credentials
device_secretAnonymous primary credential (issued once via POST /api/v1/devices, stored in the keychain)body { "device_secret": "<urlsafe_base64>" } (no prefix)

2FA state machine

[inactive] --setup_otp!--> [key generated] --enable_otp!(code)--> [active]
[active] --disable_otp!(current_code)--> [inactive]
[active] --login_with(otp_code)--> session.otp_verified_at = Time.current
[active] --login_with(backup_code)--> one backup code consumed

Passkey + User Verification → equivalent to OTP, sets otp_verified_at automatically.

Sub Stability & canonical_sub

  • sub — the user.id fixed permanently at grant time. OIDC stable.
  • canonical_sub — the currently live user.id. Updated on a merge. The RP references it when verifying a token.
  • linked_subs — the list of absorbed user.ids. Included in the survivor's userinfo.

Details: Sub policy, RP Migration Guide.

primary_user_id  (survivor canonical)
linked_user_id   (absorbed, UNIQUE)
merged_via       ("t1_device_link" | "t2_email_match" | "t3_otp")

A DB trigger blocks chains/cycles → canonical resolution is 1-hop. For T1/T2/T3 behavior: Account Merge.

Next steps

Identity가 제품의 신뢰를 만듭니다.