Skip to content

Anonymous Grants

logi's anonymous-first sign-up flow (v0.4) lets users use the logi app even before they go through email or SSO. By default, however, external RPs are blocked from receiving a grant from an anonymous user — RPs integrate on the premise that they receive a "verified subject" from logi.

allow_anonymous_grants is an opt-in flag that lets an RP deliberately relax that premise.

When to turn it on

Typical use cases:

  • Guest-mode UX — services that offer a demo or trial before sign-up. The RP first stores the anonymous user's work, then connects that data once the user later promotes to an identified account via SSO.
  • Collection-first RPs — cases where the RP collects additional identifying information such as phone or email on its own to enrich the subject. logi does not know "who this person is" — it only guarantees "which device they came from."
  • B2B SDK / embed — cases where the host app already has its own user context and logi provides only an auxiliary signature.

When not to turn it on (most ordinary RPs):

  • Services that require payment, healthcare, finance, or legal identification.
  • Simple SSO integrations where the RP collects no identification beyond the logi sub.

Configuration

The developer who owns the RP turns this on directly in the developer console — it is not a flow where you ask an operator or have an admin enable it for you. Developer console → app edit form → the "Allow anonymous user sign-in" toggle.

OTP step-up (re-authentication)

Changing the flag requires a recent (within 5 minutes) OTP re-verification. Because this is a sensitive setting, you must complete OTP verification again immediately before the toggle saves, even if the session is still alive. That said, developers who have not enrolled in OTP are not blocked — they continue to follow the existing lenient policy.

Cannot be toggled via PAK / CLI / MCP

This flag is available only in the web console (and the admin iOS app). It cannot be changed through machine credential channels such as PAK / CLI / MCP — those channels cannot perform an interactive OTP step-up. Even a developer working from the CLI must turn this particular toggle on in the web console.

Admin API (operator override)

The path for an operator to force it ON/OFF from the admin iOS app (or the admin API) remains available. However, you must use the dedicated whitelisted action rather than an ordinary PATCH (allow_anonymous_grants is included in EDITABLE_FIELDS):

PATCH /api/v1/admin/applications/:id/edit_fields
{ "fields": { "allow_anonymous_grants": true }, "action_request_nonce": "<nonce>" }

The request body is wrapped under the fields key (not application). There is no prefix like /admin/oauth_applications/... (everything is under /admin/applications/...). Authentication is an admin JWT plus step-up (action_request_nonce).

The default is false. Because anonymous means accepting an unverified subject (sub), turning it on increases the abuse/spam surface — which is why it is OFF by default. The change takes effect immediately and does not affect existing grants.

The response when an anonymous user is blocked

When an anonymous user attempts to consent while the RP has allow_anonymous_grants=false (the default), logi returns HTTP 403 with the following 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": "계정 설정 열기"
  }
}

What each field is for:

FieldPurpose
error_descriptionA user-facing message in Korean. The RP name is baked in — it makes clear that the block comes from the RP's policy, not from logi.
application_nameFor when you only need the RP name on its own (for example, to compose custom copy).
requires_developertrue when a self-RP (the 1pass console) requests a developer-only scope such as console:manage. false otherwise.
remediation.actionEither link_identity (add Apple/Google/email) or promote_to_developer (promote to developer mode). The mobile sheet branches on this.
remediation.user_facing_labelThe label for the "Open account settings" CTA button.

Mobile / native RP developers

Rather than surfacing the message verbatim, we recommend combining application_name + remediation.action and rewriting it to match your own app's tone. The Korean copy logi returns is a safe fallback.

QR login (mobile → desktop) also respects the flag

QR login respects allow_anonymous_grants exactly as native authorize does. When the flag is ON, an anonymous user can complete the QR authorization as-is. When it is OFF, the anonymous QR attempt is blocked, prompting the user to sign in or promote.

When the logi app scans a QR shown by a desktop browser and calls POST /api/v1/oauth/qr/:id/approve, an anonymous user with the flag OFF receives a 403 with the same anonymous_not_allowed shape as above (application_name + remediation). The QR flow does not include promotion.resume_token, though — the QR session is itself the resume vehicle, so once the user finishes promotion and calls /approve once more with the same session_uuid, the flow continues.

Just-In-Time (JIT) Promotion

The 403 response also comes with a promotion object. With this object, the logi app (or an RP that embeds the logi SDK) can complete Apple, Google, or email registration inline, without taking the user out of the OAuth flow, and continue the same flow. From the RP's point of view, nothing changes — it receives the usual ?code=&state= callback.

The promotion object added to the response

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
  }
}
FieldMeaning
promotion.reasonidentified_account (only identified accounts are accepted) or developer_role (developer mode required — for a self-RP console:manage request)
promotion.methods[]The registration options the mobile sheet renders. Branch on kind. When the reason is developer_role, only email_password is returned (developer mode requires a verified email)
promotion.resume_tokenA single-use JWT signed by logi with a 5-minute TTL. It bundles the original client_id / redirect_uri / state / code_challenge / scope
promotion.resume_endpointThe path to POST to after registration completes (/api/v1/oauth/authorize/resume)
promotion.resume_expires_inIn seconds (300 = 5 minutes). If you do not finish within it, the user has to start over

Flow (sequence)

mermaid
sequenceDiagram
  participant App as RP app
  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: Show the inline promotion sheet
  App->>Apple: Request 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 (the same user the resume_token was issued to).

Body: { "resume_token": "<JWT>" }

Responses:

StatusBodyMeaning
201{ code, state, redirect_uri }Success — the code can be delivered to the RP callback
400{ error: "invalid_request" }resume_token is missing
401{ error: "unauthenticated" }No PAK
403{ error: "resume_user_mismatch" }The user in the resume_token ≠ the user of the current PAK. The account changed — start over from the beginning
403{ error: "developer_required" }Registration succeeded, but the scope also required developer mode
422{ error: "promotion_incomplete" }The token is fine, but user.anonymous is still true. The registration step did not finish
422{ error: "resume_token_expired" }Past 5 minutes
422{ error: "resume_token_already_used" }Already redeemed once
422{ error: "invalid_resume_token" }Signature mismatch / malformed / a reason other than expiry

Security model

  • Single use: once resume_token is redeemed, its jti is marked in the cache to block reuse. Even if it is exposed in a crash log or screenshot, it cannot be used twice.
  • User binding: the user.id at issuance time is baked into the payload. If the PAK switches to a different account during registration, it fails closed with resume_user_mismatch.
  • Replays only server-verified parameters: the replay uses the canonical client_id / redirect_uri / state / scope / code_challenge that logi parsed and baked into the token itself — not the raw body the client sends. A grant cannot be hijacked by tampering with code_challenge.
  • 5-minute TTL: it assumes the registration flow realistically finishes within a minute. A longer window makes it safer to just have the user retry, since they tend to get confused by deep links.

From the RP's point of view?

You need to change nothing. Just do the usual /oauth/authorize?code=&state= callback → /oauth/token exchange. logi handles the anonymous user's promotion inside the OAuth flow and always hands the RP a code for an identified user.

The audit log for a grant issued after JIT promotion records via: "jit_promotion_resume", so operators can trace "how this consent came in."

How an anonymous user's grant differs

When an RP with allow_anonymous_grants=true receives a grant from an anonymous user, the userinfo response looks like this:

json
{
  "sub":          "9182",
  "canonical_sub": "9182",
  "is_canonical": true,
  "anonymous":    true,
  "email":        "anon+abc123@1pass.internal",
  "email_verified": false,
  "linked_subs":  []
}

The key differences:

  • anonymous: true — the RP can recognize that this user is anonymous.
  • email is an internal placeholder (anon+<hash>@1pass.internal) — not a real email.
  • email_verified: false.

When the RP stores this user in its own domain model, it is safer to mark it as an "anonymous placeholder."

Behavior on anonymous → identified promotion

When an anonymous user later promotes via Apple/Google SSO, logi automatically:

  1. Fills in apple_sub or google_sub on the same user.id.
  2. Flips anonymous: false.
  3. Replaces email_address with the real email if the provider supplied one.
  4. Bakes in a previously_anonymous: true claim for the lifetime of the user.
  5. On the next token rotation, the RP receives the changed claims.

What the RP needs to do at this point:

  • Detect that userinfo's anonymous flipped from true to false and mark its own user row as "promoted."
  • Keep the data collected during the anonymous period in the promoted user's view as-is.

A separate event is not emitted via webhook — promotion is a simple update of the same user.id, not a merge. The RP must notice it at the time of token rotation.

Merging an anonymous user

Scenarios where an anonymous user is absorbed by another user or absorbs another user:

  • An anonymous user matches an existing user with the same email during SSO promotion → triggers T2. The anonymous user is absorbed and disappears, consolidated into the survivor's data. The RP receives user.merged.
  • A user explicitly absorbs an anonymous account via T3 → possible but uncommon. It works as long as dual PoP passes.

The data of the absorbed anonymous user remains as linked_user_id and is recorded in the RP-side logi_identity_links.

These are items worth reviewing for an RP that has turned on allow_anonymous_grants=true to maintain user trust. Legal requirements (such as a privacy policy) require separate review under each RP's applicable laws.

  • [ ] Manage anonymous users' data under a separate lifecycle (retention period, policy for cleaning up anonymous placeholders, etc.).
  • [ ] It is safer to not expose, or to restrict, operations that carry payment or legal liability for users where anonymous: true.
  • [ ] Design so that data from the anonymous period connects naturally at promotion time (the flip to anonymous: false).
  • [ ] Test in advance that an anonymous user's data also merges correctly into the canonical record when you receive user.merged.
  • [ ] State in your privacy policy the policy for "anonymous use + connecting data on subsequent promotion" (review of applicable laws recommended).

Security considerations

  • The only proof of identity for an anonymous grant is the device's device_secret. As a result, the device's security level becomes the grant's security level, so we recommend not using anonymous grants for operations that require a high security level.
  • If an anonymous user does not link SSO within the 30-day grace period, they are hard-deleted by PurgeUserJob. At that point the RP receives the user.grants_revoked event. To recover within the grace period, the user uses the POST /api/v1/account_recoveries flow.
  • We recommend that operators periodically review the list of RPs where oauth_applications.allow_anonymous_grants is true.

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