OpenID Connect
|
This section documents OAuth 2.0 (RFC 6749) as amended by the OAuth 2.0 Security Best Current Practice (RFC 9700 / BCP 240), the OAuth 1.0 Protocol (RFC 5849) for historical context, and OpenID Connect Core 1.0, as published at the IETF Datatracker and the OpenID Foundation specifications — and, on the Spring pages, Spring Boot 4.1.x and Spring Security 7.1.x as published at the Spring Security reference documentation — which are the references these pages are written and verified against. OAuth 2.1 is still an Internet-Draft ( This content was generated with the assistance of AI and should be verified against those official specifications before being relied on in production. This section’s bibliography lists the reference material consulted while preparing these pages. |
OAuth 2.0 (RFC 6749) is a delegated authorization framework: it lets a client obtain a token scoped to some
resource, and says nothing about who the resource owner is. OpenID Connect (OIDC) Core 1.0 is the identity layer
built on top of it: the same authorization-code exchange described in
Authorization Code and PKCE also returns an ID token — a signed JSON Web Token that is a verifiable statement, from the authorization server to the client, that a
specific end user authenticated at a specific time. Everything on this page is additive to plain OAuth 2.0: an
OIDC-unaware client sees an ordinary authorization_code grant; an OIDC-aware client adds openid to scope
and gets an ID token alongside the access token.
Turning OAuth into authentication: scope=openid
A client asks for identity by including the openid scope in the authorization request, alongside any of the
standard OIDC scopes that request additional claims:
| Scope | Claims it adds to the ID token or UserInfo response |
|---|---|
|
Required to trigger OIDC behaviour at all; guarantees |
|
|
|
|
|
|
|
|
|
Requests a refresh token be issued alongside the tokens above (subject to AS policy) |
GET /oauth2/authorize?response_type=code
&client_id=web-app
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20profile%20email
&state=af0ifjsldkj
&nonce=n-0S6_WzA2Mj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: auth.example.com
An authorization server that does not recognise openid is free to ignore it and behave as a plain OAuth 2.0
server — there is no wire-level breakage, which is exactly why OIDC could be layered onto RFC 6749 without a
new major version.
The ID token and its validation rules
The token endpoint response for an OIDC request adds an id_token member next to access_token and (if
requested) refresh_token:
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9...",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "8xLOxBtZp8...",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9.eyJpc3MiOiJodHRwczovL2F1dGguZXhhbXBsZS5jb20iLCJzdWIiOiIyNDgyODk3NjEwMDEiLCJhdWQiOiJ3ZWItYXBwIiwiZXhwIjoxNzU3ODY4ODAwLCJpYXQiOjE3NTc4NjUyMDAsIm5vbmNlIjoibi0wUzZfV3pBMk1qIiwiYXV0aF90aW1lIjoxNzU3ODY1MTk4LCJhY3IiOiJ1cm46bWFjZTppbmNvbW1vbjppYXA6c2lsdmVyIiwiYW1yIjpbInB3ZCJdfQ.signature"
}
Decoded, the payload carries the claims a client must check before trusting anything else in it:
| Claim | What it means, and the check the client must perform |
|---|---|
|
The issuer that signed the token; must equal the discovery |
|
Subject identifier — the stable, unique-per-issuer identifier for the end user; use it, not |
|
Must contain the client’s own |
|
Standard JWT expiry / issued-at; reject an expired token and one issued implausibly far in the past |
|
Must equal, byte for byte, the |
|
Present only in flows that also return an access token / code alongside the ID token (the hybrid |
signature |
Verify using the key identified by the JWS header’s |
|
Validating the ID token is the client’s job, done once, right after the token response arrives. It is not the same operation as a resource server validating an access token on every API call — see ID Tokens vs. Access Tokens for why conflating the two is the single most common OIDC implementation mistake, and JWT and JOSE for the full JWS/JWT validation pipeline and the RFC 8725 hardening checklist that applies to both token types. |
Common ID token validation failures
A resource server or client that logs which validation check failed, rather than a bare "invalid token", turns most integration problems into a two-minute fix instead of a support ticket:
| Symptom | Usual cause |
|---|---|
|
Discovery document was cached from a different environment (staging vs. production issuer), or the AS sits behind a reverse proxy that rewrites the externally visible issuer differently from what the AS itself puts in |
|
The client validated a token that was actually issued for a different registered client — often a copy-paste of another service’s |
|
The client’s session-bound |
Signature verification fails after a deploy |
The authorization server rotated its signing key and the resource server’s cached JWKS is stale — see "JWKS publication and key rollover" in Discovery, Metadata and Client Registration |
|
Clock skew between the client/resource-server host and the authorization server — allow a small (a few seconds) tolerance rather than none, but never tolerate more than that |
|
A long-lived SSO session was reused instead of a fresh login — correct if the client only asked for |
UserInfo: fetching more claims after the fact
The ID token is meant to stay small. When a client needs claims that were not worth putting in every ID token (a fresh profile picture URL, a phone number), it calls the UserInfo endpoint with the access token as a bearer credential:
GET /userinfo HTTP/1.1
Host: auth.example.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6ImFiYzEyMyJ9...
{
"sub": "248289761001",
"name": "Jane Doe",
"given_name": "Jane",
"family_name": "Doe",
"email": "jane.doe@example.com",
"email_verified": true
}
The sub returned here must match the sub in the ID token issued in the same session — an authorization
server that ever returns a mismatched sub between the two has a bug serious enough to treat as a security
incident. UserInfo can also be requested with claims signed and optionally encrypted as a JWT
(Content-Type: application/jwt) rather than plain JSON, negotiated via client registration.
response_type: how the ID token is delivered
response_type controls both which artefacts (code, id_token, token) the authorization endpoint issues
and where they land — the query string (safe) or the URL fragment (visible only to the browser, never sent to
the server, but readable by any script on the page and preserved in browser history).
response_type |
Flow name | Returned from the authorization endpoint | Status |
|---|---|---|---|
|
Authorization code |
An authorization |
Current — the only one to use for new clients |
|
Implicit (identity-only) |
An |
Legacy — inherits the implicit grant’s fragment exposure; superseded by |
|
Implicit |
|
Legacy — the exact shape RFC 6749’s implicit grant warned against; removed as a pure OAuth grant by the in-progress OAuth 2.1 consolidation ( |
|
Hybrid |
|
Legacy/niche — occasionally used so the client can start a session before the token exchange completes; carries the same fragment-leakage exposure for the |
|
Hybrid |
|
Legacy/niche — rarely implemented; the access token in the fragment has no upside once |
|
Hybrid |
All three, split between fragment and query string |
Legacy/niche — combines the exposure of both hybrid variants above |
Everything except code puts a live credential (an ID token, or worse, an access token) directly in a browser
redirect, with all the risks Legacy Implicit and
Password Grants documents for OAuth’s plain implicit grant: no client authentication at the token-issuing step,
exposure through Referer headers, browser history and proxy/server logs, and no refresh token. New OIDC
clients should request response_type=code exclusively and read id_token from the token response, exactly as
shown above — there is no remaining reason to take an ID token from the front channel.
Controlling the authentication request
Beyond the plain OAuth parameters (client_id, redirect_uri, scope, state, PKCE’s code_challenge /
code_challenge_method), OIDC defines authentication-request parameters that steer what happens at the
authorization server’s login page:
| Parameter | Effect |
|---|---|
|
The AS must not display any UI; succeed silently using an existing session or fail with |
|
Force a fresh, interactive re-authentication even if a session already exists — the mechanism a resource server’s step-up challenge (RFC 9470) tells a client to invoke; see Authentication Methods: Passwordless and 2FA for the full step-up walkthrough. |
|
Force the consent screen even if the user already approved these scopes |
|
Force the account picker on an AS that supports multiple simultaneous sessions |
|
Maximum acceptable number of seconds since the last active authentication; if exceeded, the AS must re-authenticate and the returned |
|
A hint (username, email, phone) that pre-fills the login form — never sufficient to authenticate on its own |
|
Space-separated, most-preferred-first list of BCP 47 locale tags for the login UI |
|
Space-separated list of requested Authentication Context Class Reference values, most preferred first — the client’s way of asking for a specific authentication strength (a particular |
|
An ID token from a prior session, hinting who is (or was) logged in — also used on the logout endpoint, see Logout and Session Management |
None of these are enforceable by the client — they are requests the authorization server is free to honour,
partially honour, or ignore, exactly like scope. The client’s job is to read back what actually happened from
acr, amr and auth_time in the returned ID token, not to assume the request was granted verbatim.
Requesting individual claims with the claims parameter
The scope-to-claims mapping in the first table (profile, email, address, phone) is coarse: asking for
profile releases the whole bundle of name-related claims, whether the client needs all of them or just one. The
optional claims request parameter lets a client ask for individual claims directly, and say whether each one
must be delivered in the ID token or is acceptable from UserInfo, and whether it is essential to the request:
{
"userinfo": {
"given_name": {"essential": true},
"email": {"essential": true},
"picture": null
},
"id_token": {
"acr": {"values": ["urn:mace:incommon:iap:silver"]},
"auth_time": {"essential": true}
}
}
This JSON object is passed, URL-encoded, as the claims authorization-request parameter (or by reference via
claims_uri on authorization servers that support it). "essential": true tells the AS this claim matters
enough that, if it cannot be released (a scope not granted, a provider that does not collect it), authentication
should be treated as incomplete rather than silently proceeding without it — acr under id_token here is the
same mechanism a step-up challenge uses to demand a specific authentication strength. Most clients never need
this level of control and get everything they need from scope alone; it exists for the cases where a client
needs one specific claim guaranteed in the ID token itself rather than fetched separately from UserInfo.
auth_time, acr and amr: what actually happened at login
Three ID token claims report the authentication event itself, distinct from the token issuance:
| Claim | Meaning |
|---|---|
|
Unix timestamp of when the end user last actively authenticated (not when the token was issued — a long-lived SSO session can reuse an old |
|
Authentication Context Class Reference — an opaque string naming the authentication policy that was satisfied (an AS-defined identifier, or a public one such as the NIST/InCommon Silver/Gold assurance-profile URNs); the client compares it only for equality against what it asked for in |
|
Authentication Method Reference — an array of RFC 8176 tokens identifying which methods were used, e.g. |
|
|
A resource server that requires a stronger authentication event than the current access token reflects issues a
step-up challenge naming the acr_values and/or max_age it needs (RFC 9470,
insufficient_user_authentication); the client re-runs the authentication request with prompt=login and those
parameters, and inspects acr/amr/auth_time on the new ID token to confirm the upgrade actually happened.
Pairwise vs. public subject identifiers
sub identifies the end user, but OIDC lets an authorization server choose how it is shaped per client:
| Subject type | Behaviour |
|---|---|
|
The same |
|
The AS derives a different, stable |
pairwise needs a sector_identifier_uri (a hosted JSON list of redirect URIs sharing one derivation) when
multiple redirect_uri values would otherwise imply different sectors for the same client. Choose pairwise
for a public, multi-tenant identity provider where cross-client correlation is a privacy concern (most social
login and consumer IdPs default to it); public is normal and simpler for an authorization server run for a
single organisation’s own applications.
"Logged in just now" vs. "the client holds a token"
These are two different facts, and OIDC keeps them separate on purpose:
-
The user authenticated is a claim about a past event — captured once, in the ID token’s
auth_time/acr/amr, at the moment the authorization code was exchanged. It does not change as long as the client keeps using the same login session. -
The client holds a valid token is a claim about the present — an access token can still be valid long after the authentication event, can be refreshed without the user doing anything, and (for a JWT access token) says nothing about when anyone last typed a password. Silently refreshing a token must never be treated as "the user is here right now": only a fresh
auth_time(obtained viaprompt=login/max_age) proves that.
A once-proposed OIDC mechanism blurred this line: early OpenID Connect drafts — and the 2012 O’Reilly book Getting Started with OAuth 2.0, which documents that draft era — described a dedicated "check ID endpoint" that a client could call to validate an ID token and, implicitly, check on the underlying session. That endpoint never shipped: OIDC Core 1.0, as finalised, dropped it entirely in favour of local, offline ID-token validation (the rules in the table above) plus the UserInfo endpoint for anything that needs a live round trip. Any material that still describes a "check ID endpoint" as part of OpenID Connect is describing a pre-final draft, not the shipped specification.