Skip to content

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 /userinfo response.

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

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. openid is 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. openid is the scope; OpenID is a different (and unrecognised) scope.
  • openid is the magic that turns OAuth into OIDC. Without openid in the request, no ID Token is issued. The library defaults to OIDC (the openid scope 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.New rejects requests for scopes not registered through WithScope (or built-in OIDC standard scopes), returning invalid_scope per 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 itselfiss, 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 claimsname, 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:

ScopeClaims released
openidsub
profilename, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
emailemail, email_verified
addressaddress
phonephone_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.

go
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's scopes_supported. Use this for scopes any vetted client can request.
  • op.InternalScope(name) — registers a scope that is omitted from scopes_supported (RFC 8414 §2 / OIDC Discovery §3 explicitly permit the omission). Acceptance is still governed by the scope's AllowedClients list. 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?

QuestionAnswerExample
"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.