Scopes & Claims
What an application may ask for, and what ends up in the token
The mental model
Four concepts, in the order they apply to a request:
| Concept | Question it answers | Where it lives |
|---|---|---|
| Scope | What may this application ask for? | Application → Allowed Scopes |
| Consent | What has the user (or org) agreed to release? | Consent screen / Manage Consents |
| Claims | What data does each granted scope actually release? | Scope → Claims field |
| Claim source | Where does each claim's value come from? | Built-ins, metadata, expressions, providers |
A scope is a label on a bundle of claims. Requesting profile does not
magically add a name to the token — it does so because the profile scope's Claims
field lists name given_name family_name preferred_username. Change that field and you
change what the scope releases.
Scope names and namespaces
Standard OIDC scopes are bare and reserved: openid, profile,
email, phone, offline_access, roles.
Scopes your organisation defines are namespaced by your organisation handle, so two
organisations can both define orders:read without colliding:
# anatomy @acme/billing:read └─ namespace └─ resource : action
Each scope has three forms — you normally only type the first:
| Form | Example | Used for |
|---|---|---|
| Display | @acme/billing:read | What you type and see in the UI, tokens, consent |
| Global id | login.example.com/acme/billing:read | Uniqueness across servers (server host is the authority) |
| Verified | acme.com/billing:read | Once the org verifies a domain — portable across servers |
billing:read) or qualified (@acme/billing:read) — Orion treats them as the
same scope when matching. Scope strings you send are never rewritten, so anything that compares
token scope values literally keeps working.
The Claims field
A scope's Claims field is a space-separated list of claim names. Each name is routed to a claim source by its prefix — this is the part that is easy to miss:
| Prefix | Example | Value comes from |
|---|---|---|
| none | email | Built-in user/org field, else a static per-user Custom Claim |
meta: | meta:department | User metadata (app-scoped value wins over org-wide) |
expr: | expr:is_premium | A computed Claim Definition (sandboxed expression) |
ext: | ext:entitlements | An external Claim Provider, called live or synced |
The emitted claim name drops the prefix — meta:department appears in the token as department.
Built-in claim names
sub, name, given_name, family_name,
preferred_username, email, phone_number,
org_id, org_name, roles.
iss, aud,
exp, iat, jti, scope, client_id, …)
can never be produced by metadata, expressions, or external providers — a provider cannot overwrite
sub and impersonate a user. sub and org_id are emitted once by
the server itself.
Scope fields explained
| Field | Meaning |
|---|---|
| Name | The scope identifier, e.g. orders:read. Namespaced automatically. |
| Display Name | Human label on the consent screen. |
| Description | The sentence the user reads when consenting. |
| Claims | Space-separated claim names this scope releases (see above). |
| Required | Always included in tokens for the app, whether or not requested. |
| Standard | Marks the built-in OIDC scopes. Standard scopes cannot be deleted. |
Scopes with no Claims are resource scopes — they release no data, they authorise an action.
devices:provision is one: holding it lets an application call the device provisioning
API. Consent still applies; the API additionally checks a provisioning grant.
Worked example
An organisation wants applications to see a user's department and whether they are a manager.
- Set the metadata on users: Roles & Claims → member → Metadata →
department = Engineering. - Define a computed claim: Claims → New → name
is_manager, type Boolean, expressioncontains(roles, "Manager"). - Create the scope: Scopes → New → name
staff, Claimsmeta:department expr:is_manager. - Allow it on the application: Applications → app → OAuth settings → Allowed Scopes.
A token issued for that scope then contains:
{
"sub": "62B3…",
"scope": "openid @acme/staff",
"department": "Engineering",
"is_manager": true
}
Claim values resolve at issuance, so changing a user's metadata or roles affects the next token without touching the scope. With Orion Verified tokens it affects the very next request.
If a claim is missing
- Is the scope in the application's Allowed Scopes? If not the request fails with
invalid_scope. - Was the scope actually requested and consented to? Check the token's
scopeclaim. - Is the claim name listed in the scope's Claims field, with the right prefix?
- Does the value exist — metadata set for that user, expression returning non-null, provider reachable?
- Is the name reserved? Dynamic sources are silently filtered from reserved names.
- Check
https://login.shanecraven.com/oauth/{org}/userinfo— it resolves through the same pipeline as tokens.