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_scopesat 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
| scope | Fields returned by userinfo | Notes |
|---|---|---|
profile:basic | sub, preferred_name, full_name | Basic 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 profile ≈ profile:basic as equivalent). |
email | sub, email, email_verified | |
phone | sub, phone_number | Requires separate consent |
address | sub, address, postal_code | Requires separate consent |
identity:level | sub, identity_verified_level (0/1/2/3) | Identity verification level. New apps must request it explicitly (legacy apps get it automatically with profile). |
openid | Issues an id_token + sub | Enables 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.writeThese 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
{
"oauth_application": {
"redirect_uris": ["https://app.example.com/cb"],
"allowed_scopes": ["profile:basic", "email"]
}
}Configuring scopes
logi apps edit <id> --add-scope phoneapp = OauthApplication.kept.find_by(name: "your_app")
app.set_scopes!(["openid", "profile:basic", "email", "phone"])⚠️ Warning: OAuth is rejected if an app's
allowed_scopesis empty An app with emptyallowed_scopesis rejected at authorize time withinvalid_scope: no allowed scopes. You must enable the scopes you want to use (openid,profile:basic,openid profile:basic emailbaseline when no scope is specified, API/CLI registration registers only theallowed_scopesincluded in the request (empty if unspecified), and the MCP/DCR connector receives only theopenid agent:read agent:writeceiling (no identity scopes such asprofile:basic/
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).
| Case | Default (block) | log_only / alert |
|---|---|---|
| All requested scopes are registered | ✅ Proceed | ✅ Proceed |
| Some are unregistered | ❌ invalid_scope | ⚠️ Drop the unregistered ones, proceed with the registered ones |
| All are unregistered | ❌ invalid_scope | ❌ invalid_scope |
A required: true scope is missing from the effective set | ❌ invalid_scope | ❌ invalid_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,email2. Webhook scope.drift_detected (HMAC-SHA256 signed, fired once per (app_id, scope_name) pair):
{
"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/1.1 200 OK
X-Logi-Scope-Drift: address,phoneif drift = res["X-Logi-Scope-Drift"]
Rails.logger.warn("[logi] scope drift: #{drift}")
end4. 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
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 scope | Behavior |
|---|---|
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 revoked | Show 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).