Scopes and claims
Scope and claim are the two words OIDC newcomers most often confuse. They are not the same thing.
- A scope is what the client asks for in the authorize request. It is one of a small set of identifiers the OP advertises in
scopes_supported. - A claim is a key-value pair the OP returns — inside an ID Token, inside an access token, or inside a
/userinforesponse.
Scopes are the request; claims are the response. Granting a scope is what causes the OP to release the matching claims.
Mental model in 30 seconds
- Scope = "I want to see your email." —
scope=openid email - Claim =
"email": "alice@example.com"— the value that comes back. - One scope can release several claims; the OIDC standard map below shows how.
Specs referenced on this page
- RFC 6749 — OAuth 2.0 §3.3 (scope), §5.2 (
invalid_scope) - RFC 8414 — Authorization Server Metadata (
scopes_supported) - RFC 8707 — Resource Indicators for OAuth 2.0
- OpenID Connect Core 1.0 — §5.1 (standard claims), §5.4 (scope-to-claim map), §5.5 (claims request)
- OpenID Connect Discovery 1.0 — §3 (
scopes_supported,claims_supported)
Scope: what the client asks for
The scope parameter on /authorize is a space-separated list of opaque identifiers. The user consents to the set; the OP records the granted set on the resulting authorization code, then on the access token, refresh token, and ID Token.
GET /authorize?response_type=code&client_id=...&scope=openid%20profile%20email&...A few observations beginners often miss:
- Scope identifiers are opaque to the protocol.
openidis special only because OIDC Core §5.4 assigns it the meaning "this is an OIDC flow"; the rest are just strings the OP and the client agree on. - Scopes are case-sensitive.
openidis the scope;OpenIDis a different (and unrecognised) scope. openidis the magic that turns OAuth into OIDC. Withoutopenidin the request, no ID Token is issued. The library defaults to OIDC (theopenidscope is required);op.WithOpenIDScopeOptional()flips it to pure OAuth 2.0.- The user can decline some scopes. The granted set may be a subset of the requested set; the OP advertises which scopes were actually granted on the issued tokens.
- Unknown scopes fail.
op.Newrejects requests for scopes not registered throughWithScope(or built-in OIDC standard scopes), returninginvalid_scopeper RFC 6749 §5.2.
Claim: a key-value in a token or userinfo response
A claim is a single field inside a JSON object the OP signs (or, for /userinfo, returns plain). OIDC Core §5.1 catalogues a long list. They split structurally into two groups:
- Standard claims about the request itself —
iss,aud,exp,iat,nbf,sub,nonce,auth_time,acr,amr,azp. These describe the token, not the user. The OP fills them in regardless of which scopes were granted. - User claims —
name,family_name,email,email_verified,phone_number,address,picture, etc. These are about the end-user. The OP releases them only when the granted scope authorises it.
The first group lives on every token. The second group is what scope grants control.
OIDC Core scope-to-claim map
OIDC Core §5.4 fixes the mapping for the standard scopes. The library implements it verbatim:
| Scope | Claims released |
|---|---|
openid | sub |
profile | name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at |
email | email, email_verified |
address | address |
phone | phone_number, phone_number_verified |
offline_access | (no claim — controls the user-facing consent prompt for refresh-token issuance and which refresh-token TTL bucket applies) |
A request for scope=openid email therefore releases sub, email, and email_verified — and nothing else from the user-claim catalogue. A request for scope=openid profile email adds the entire profile group.
offline_access does not release a claim
offline_access is the OIDC standard scope that signals "the client wants to keep working when the user is not present" — i.e. the user-facing consent prompt for refresh-token issuance. It releases no user claim. In this library it routes the resulting refresh token to a separate TTL bucket (op.WithRefreshTokenOfflineTTL) so stay-signed-in chains can outlive everyday short-session refreshes; see Refresh tokens.
Custom scopes
The library lets embedders register additional scopes through op.WithScope. Every custom scope is a op.Scope struct describing the wire identifier, the consent-prompt UI text, the claim list it releases, and (optionally) which clients may request it.
op.WithScope(op.PublicScope("read:projects", "Read your projects")),
op.WithScope(op.Scope{
Name: "write:projects",
Title: "Write to your projects",
Description: "Create and modify projects on your behalf.",
Public: true,
Claims: []string{"projects:permissions"},
AllowedClients: []string{"trusted-client-1"},
}),Two helpers cover the common shapes:
op.PublicScope(name, label)— registers a scope that is advertised in the discovery document'sscopes_supported. Use this for scopes any vetted client can request.op.InternalScope(name)— registers a scope that is omitted fromscopes_supported(RFC 8414 §2 / OIDC Discovery §3 explicitly permit the omission). Acceptance is still governed by the scope'sAllowedClientslist. Use this for scopes that should not be discoverable but are honoured for specific clients.
Standard OIDC scopes are recognised automatically with built-in defaults. Calling WithScope for a standard name (e.g. email) overrides the built-in entry — typically to attach translations or extra claim mappings — but registering one with Public: false fails op.New so the discovery document never advertises an OIDC scope as missing.
See Use case: Scopes for the full walkthrough including i18n labels, claim mapping, and consent-prompt rendering.
Scopes vs RFC 8707 resource indicators
A common modelling question: do you split a permission with a new scope, or with a new audience?
| Question | Answer | Example |
|---|---|---|
| "Is this a different kind of permission the same RS understands?" | Use a scope. | read:projects vs write:projects against the same project API. |
| "Is this a different resource server that needs its own audience?" | Use RFC 8707. | An access token for the project API vs an access token for the billing API; both flows want different aud values so the RS verifiers reject cross-audience tokens. |
| "Is the lifetime/format/revocation policy different per audience?" | Use RFC 8707 with per-audience format. | Admin API gets opaque (immediate revocation) while the read API stays JWT (horizontal scale). The library wires this through op.WithAccessTokenFormatPerAudience. |
Scopes describe breadth of permission; resource indicators describe target audience. A single authorize request can carry both — scope=openid read:projects&resource=https://billing.example.com&resource=https://projects.example.com asks the OP to mint two access tokens, one per resource, each carrying the granted scope set.
See Use case: Claims request for the OIDC §5.5 claims parameter, which lets the RP request specific claims independent of scope grouping; see Access token format for the per-audience format trade-off.
Read next
- ID Token, access token, userinfo — what each artefact actually contains and how scopes shape it.
- Use case: Scopes — i18n labels, claim mapping, allowed-clients, and the consent-prompt surface.
- Use case: Claims request — the OIDC §5.5
claimsparameter for fine-grained claim selection.