Scope Reference
A scope is a space-separated string (profile email phone, not comma-separated).
Standard scopes
| scope | Fields returned by userinfo | Notes |
|---|---|---|
profile:basic | sub, preferred_name, full_name | Basic identity. The profile alias is accepted, but we recommend registering and requesting profile:basic. |
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", "email"]
}
}Configuring scopes
logi apps edit <id> --add-scope phoneapp = OauthApplication.kept.find_by(name: "your_app")
app.set_scopes!(["openid", "profile", "email", "phone"])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,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 email:
| Requested scope | Behavior |
|---|---|
Same or narrower (profile) | Skip the UI, issue the code immediately |
Expanded (profile email phone) | "NEW" badge + additional consent |
| After consent has been revoked | Show the consent screen again |
How to request
GET /oauth/authorize?...&scope=profile+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).