Skip to content

Scope Reference

Source: en/oauth/scopes.md · Live: https://docs.1pass.dev/en/oauth/scopes LLM-sanitized: internal links absolutized, VitePress containers → admonitions, line numbers in the Jump-to Index reference this rendered file (1-indexed).

📍 Jump-to Index

  • L24-L36: ## Standard scopes
  • L37-L47: ## Custom scopes
  • L48-L72: ## allowed_scopes at app registration
    • L59-L72: ### Configuring scopes
  • L73-L122: ## Scope drift handling
    • L86-L122: ### How to detect it
  • L123-L130: ## Marking a scope required
  • L131-L140: ## Re-authorization UX
  • L141-L148: ## How to request

Scope Reference

A scope is a space-separated string (profile:basic email phone, not comma-separated).

Standard scopes

scopeFields returned by userinfoNotes
profile:basicsub, preferred_name, full_nameBasic identity. Use exactly profile:basic for both registration and requests. The scope drift check (default block) does an exact string comparison, so bare profile is treated as unregistered and rejected (only the userinfo claim mapping treats profileprofile:basic as equivalent).
emailsub, email, email_verified
phonesub, phone_numberRequires separate consent
addresssub, address, postal_codeRequires separate consent
identity:levelsub, identity_verified_level (0/1/2/3)Identity verification level. New apps must request it explicitly (legacy apps get it automatically with profile).
openidIssues an id_token + subEnables OIDC 1.0

identity_verified_level values: 0 unverified · 1 email_verified · 2 phone_verified · 3 sp_verified. logi does not hold real-name or national ID data — it provides only the integer flag.

Custom scopes

The namespaced form is required: <namespace>:<key> (exactly one colon):

krx_listing:reviewer_role
blog:post.write

These are stored in the User#custom_claims jsonb as {namespace: {key: value}}, and merged into the id_token/userinfo when the scope is requested.

allowed_scopes at app registration

json
{
  "oauth_application": {
    "redirect_uris": ["https://app.example.com/cb"],
    "allowed_scopes": ["profile:basic", "email"]
  }
}

Configuring scopes

bash
logi apps edit <id> --add-scope phone
ruby
app = OauthApplication.kept.find_by(name: "your_app")
app.set_scopes!(["openid", "profile:basic", "email", "phone"])

⚠️ Warning: OAuth is rejected if an app's allowed_scopes is empty An app with empty allowed_scopes is rejected at authorize time with invalid_scope: no allowed scopes. You must enable the scopes you want to use (openid, profile:basic, email, etc.) under Console → App detail → Scopes for OAuth to work. The initial scopes differ by registration path — the web console registration form auto-seeds the openid profile:basic email baseline when no scope is specified, API/CLI registration registers only the allowed_scopes included in the request (empty if unspecified), and the MCP/DCR connector receives only the openid agent:read agent:write ceiling (no identity scopes such as profile:basic/email). If the scopes are empty or insufficient, enable them directly on this screen.

Scope drift handling

The default policy is block — if even one requested scope is not in allowed_scopes, the entire request is rejected with invalid_scope (a callback-safe redirect). An operator can relax this per app to log_only (silently drop the unregistered scopes and proceed with the registered subset) or alert (log_only plus an admin notification).

CaseDefault (block)log_only / alert
All requested scopes are registered✅ Proceed✅ Proceed
Some are unregisteredinvalid_scope⚠️ Drop the unregistered ones, proceed with the registered ones
All are unregisteredinvalid_scopeinvalid_scope
A required: true scope is missing from the effective setinvalid_scopeinvalid_scope

Regardless of the policy, drift is always recorded (log + drift record + a one-time webhook when a webhook_url is configured) — even when the request is rejected by block, so the operator can trace the cause.

How to detect it

1. Server logs (grep by client_id):

[oauth] scope_drift app_id=4 client_id=logi_xxx policy=block dropped=phone,address kept=profile:basic,email

2. Webhook scope.drift_detected (HMAC-SHA256 signed, fired once per (app_id, scope_name) pair):

json
{
  "event_type": "scope.drift_detected",
  "application_id": 4,
  "payload": {
    "scope_name": "phone",
    "client_id": "logi_xxx",
    "first_seen_at": "2026-04-29T01:30:00Z",
    "allowed_scopes": ["openid", "profile:basic", "email"]
  }
}

3. Token response header X-Logi-Scope-Drift — echoes the drift recorded within the last 7 days on successful token responses. Under the default block policy, a request containing drift is itself rejected with invalid_scope, so this header appears on subsequent successful token responses (drift-free requests) as a history echo:

http
HTTP/1.1 200 OK
X-Logi-Scope-Drift: address,phone
ruby
if drift = res["X-Logi-Scope-Drift"]
  Rails.logger.warn("[logi] scope drift: #{drift}")
end

4. Escalation webhook scope.drift_unresolved — fired once if the drift is still ongoing 7 days after the initial notification.

5. Developer dashboard — a "Scope drift" pin on the Apps card, with a drift table in the detail view.

Marking a scope required

ruby
app.oauth_application_scopes.create!(oauth_scope: email_scope, required: true)

If the user declines a required scope → access_denied.

Re-authorization UX

After a user has consented to profile:basic email:

Requested scopeBehavior
Same or narrower (profile:basic)Skip the UI, issue the code immediately
Expanded (profile:basic email phone)"NEW" badge + additional consent
After consent has been revokedShow the consent screen again

How to request

GET /oauth/authorize?...&scope=profile:basic+email+openid&...

Space-separated; use %20 or + when URL-encoding. The scope field in the response echoes the scopes actually granted (the user may consent to only a subset).

최종 수정:

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