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 (draft-ietf-oauth-v2-1-16, 3 September 2026) and is flagged as such everywhere it appears on these pages. It is a working-group consolidation in progress, not a published standard; nothing here should be read as saying otherwise.

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

openid

Required to trigger OIDC behaviour at all; guarantees sub and the token-integrity claims below

profile

name, family_name, given_name, middle_name, nickname, preferred_username, picture, website, gender, birthdate, zoneinfo, locale, updated_at

email

email, email_verified

address

address (a structured claim: formatted, street_address, locality, region, postal_code, country)

phone

phone_number, phone_number_verified

offline_access

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

iss

The issuer that signed the token; must equal the discovery issuer value exactly (scheme, host, path, no trailing slash mismatch)

sub

Subject identifier — the stable, unique-per-issuer identifier for the end user; use it, not email, as the account key

aud

Must contain the client’s own client_id; if it contains more than one audience, azp (authorized party) must equal the client_id

exp / iat

Standard JWT expiry / issued-at; reject an expired token and one issued implausibly far in the past

nonce

Must equal, byte for byte, the nonce value the client sent in the authorization request — this is what stops a captured ID token from being replayed into a different login session

at_hash / c_hash

Present only in flows that also return an access token / code alongside the ID token (the hybrid response_type values below); the client must recompute the hash and compare it, binding the ID token to that specific access token / code

signature

Verify using the key identified by the JWS header’s kid, fetched from the issuer’s jwks_uri (see Discovery, Metadata and Client Registration); reject alg: none and reject any algorithm other than the one the client registered to expect

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

iss mismatch

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 iss

aud does not contain the client ID

The client validated a token that was actually issued for a different registered client — often a copy-paste of another service’s client_id during configuration

nonce mismatch or missing

The client’s session-bound nonce was lost between the authorization request and the callback (e.g. a load balancer routed the callback to a different, stateless instance that never stored it) — see Security Best Practices for state/nonce handling under horizontal scaling

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

exp in the past immediately after issuance

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

auth_time older than expected

A long-lived SSO session was reused instead of a fresh login — correct if the client only asked for prompt=none/no prompt; a bug if the client asked for prompt=login and did not get a fresh auth_time back

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

code

Authorization code

An authorization code in the query string; tokens come only from a subsequent POST /token

Current — the only one to use for new clients

id_token

Implicit (identity-only)

An id_token directly in the URL fragment; no access token, no code

Legacy — inherits the implicit grant’s fragment exposure; superseded by code

id_token token

Implicit

id_token and token (access token) both in the fragment

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 (draft-ietf-oauth-v2-1-16, 3 September 2026)

code id_token

Hybrid

id_token in the fragment immediately, code in the query string for the token-endpoint round trip

Legacy/niche — occasionally used so the client can start a session before the token exchange completes; carries the same fragment-leakage exposure for the id_token half

code token

Hybrid

token (access token) in the fragment, code in the query string

Legacy/niche — rarely implemented; the access token in the fragment has no upside once code is present

code id_token token

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

prompt=none

The AS must not display any UI; succeed silently using an existing session or fail with login_required / interaction_required / consent_required. Used for silent session checks (an invisible iframe) and for CIBA-adjacent background flows.

prompt=login

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.

prompt=consent

Force the consent screen even if the user already approved these scopes

prompt=select_account

Force the account picker on an AS that supports multiple simultaneous sessions

max_age

Maximum acceptable number of seconds since the last active authentication; if exceeded, the AS must re-authenticate and the returned auth_time must reflect it

login_hint

A hint (username, email, phone) that pre-fills the login form — never sufficient to authenticate on its own

ui_locales

Space-separated, most-preferred-first list of BCP 47 locale tags for the login UI

acr_values

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 acr, see below)

id_token_hint

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

auth_time

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 auth_time)

acr

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 acr_values, never by trying to infer a strength ordering from the string itself

amr

Authentication Method Reference — an array of RFC 8176 tokens identifying which methods were used, e.g. ["pwd"], ["otp"], ["pwd","otp"], ["mfa"], ["sms"], ["hwk"], ["fpt"]

amr containing "mfa" means more than one authentication factor was used; it says nothing about which factor was primary. "amr": ["pwd", "mfa"] and even "amr": ["mfa"] alone are both perfectly consistent with a password having been entered first and a second factor stacked on top of it — 2FA/MFA is an additional security layer on top of a primary factor, never a primary authentication method and never "passwordless" in itself. A client that wants to know whether the primary factor was passwordless has to look for the specific passwordless amr values ("otp" delivered out of band, "sms", "swk"/"hwk" for a passkey or hardware key, "fpt" for a fingerprint biometric) rather than treating the presence of "mfa" as proof that no password was involved. The full enumeration of primary passwordless methods and 2FA layers, and which amr values map to each, lives in Authentication Methods: Passwordless and 2FA — this page only defines how the two claims reach the client.

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

public

The same sub value is returned to every client for a given user — simple, but it lets any two clients that both talk to the same AS correlate the same user by comparing sub values, even without collusion.

pairwise

The AS derives a different, stable sub per (client, user) pair, typically sub = SHA-256(sector_identifier + local_account_id + salt). Each client sees a consistent identifier across that user’s logins, but two different clients see two unrelated strings for the same person.

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 via prompt=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.

OIDC authorization code flow, ID token and UserInfo

sequenceDiagram participant U as Resource owner (browser) participant C as Client participant AS as Authorization server U->>C: click "Log in" C->>U: redirect to /authorize (scope=openid profile email,\nstate, nonce, code_challenge) U->>AS: GET /authorize AS->>U: login page, then consent U->>AS: authenticate + approve AS->>U: redirect to redirect_uri with code and state U->>C: GET redirect_uri (code, state) C->>AS: POST /token (code, code_verifier, redirect_uri) AS->>C: access_token, refresh_token, id_token C->>C: validate id_token (iss, aud, exp, nonce, signature) C->>AS: GET /userinfo (Authorization: Bearer access_token) AS->>C: sub, name, email, email_verified C->>U: session established