Skip to content
Start with Identity
Protocols · Advanced

How OpenID Connect Works, Message by Message

By SWI Community Team · Updated 2026-11-05 · 14 min

OpenID Connect (OIDC) is the OAuth 2.0 authorization code flow plus one extra artifact: a signed ID token that tells the relying party (RP) who logged in, when and how. On the wire that is five HTTP exchanges: discovery, the authentication request, the redirect back with a code, the token request, and optionally a userinfo call. This page shows each one with realistic, fake values, explains every parameter with its section in OpenID Connect Core 1.0 or the relevant RFC, and ends with the checks an RP must run and the mistakes that became CVEs.

For what OIDC is, its status and where it fits, see the OpenID Connect standard page. For the OAuth layer underneath, including client credentials and refresh, see how OAuth 2.0 and 2.1 work, message by message.

The sequence at a glance

# From → to Message Spec
0 RP → OP GET /.well-known/openid-configuration Discovery §4
1 RP → browser → OP Authentication request to the authorization endpoint Core §3.1.2.1
2 OP ↔ user Login, MFA, consent Core §3.1.2.3–3.1.2.4
3 OP → browser → RP Redirect with code, state, iss Core §3.1.2.5, RFC 9207 §2
4 RP → OP (back channel) Token request with code and code_verifier Core §3.1.3.1, RFC 7636 §4.5
5 OP → RP Token response: id_token, access_token, optional refresh_token Core §3.1.3.3
6 RP → OP Optional userinfo request with the access token Core §5.3

Step 0: discovery

GET /.well-known/openid-configuration HTTP/1.1
Host: login.example.com
{
  "issuer": "https://login.example.com",
  "authorization_endpoint": "https://login.example.com/authorize",
  "token_endpoint": "https://login.example.com/oauth/token",
  "userinfo_endpoint": "https://login.example.com/userinfo",
  "jwks_uri": "https://login.example.com/.well-known/jwks.json",
  "response_types_supported": ["code"],
  "subject_types_supported": ["public", "pairwise"],
  "id_token_signing_alg_values_supported": ["RS256", "ES256"],
  "code_challenge_methods_supported": ["S256"],
  "authorization_response_iss_parameter_supported": true,
  "token_endpoint_auth_methods_supported": ["private_key_jwt", "client_secret_basic"]
}
Field Why the RP cares Spec
issuer The exact string every ID token's iss must equal. It must also match the URL you fetched the document from. Discovery §3, §4.3
jwks_uri Where the signing keys live. Cache with a TTL and refetch on an unknown kid. Discovery §3
id_token_signing_alg_values_supported What the OP may sign with. Your RP should accept only the one algorithm you registered. Discovery §3
code_challenge_methods_supported Whether PKCE S256 is available (it should be). RFC 8414 §2
authorization_response_iss_parameter_supported The OP adds iss to the redirect, which defeats mix-up attacks. RFC 9207 §3

Step 1: the authentication request

The RP generates three random values per login and stores them in the user's pre-login session: state, nonce, and a PKCE code_verifier (43 to 128 characters, RFC 7636 §4.1). It sends the SHA-256 of the verifier as the challenge.

GET /authorize?response_type=code
  &client_id=s6BhdRkqt3-example
  &redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
  &scope=openid%20profile%20email
  &state=af0ifjsldkj-example
  &nonce=n-0S6_WzA2Mj-example
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256 HTTP/1.1
Host: login.example.com
Parameter Meaning Spec
response_type=code Authorization code flow, the one to use. Core §3.1.2.1
scope=openid ... openid is what makes this OIDC; profile and email request standard claims. Core §3.1.2.1, §5.4
client_id The RP's registered identifier; becomes the ID token aud. RFC 6749 §2.2
redirect_uri Must exactly match a registered value. Core §3.1.2.1, RFC 9700 §2.1
state Opaque CSRF token you check on return. Core §3.1.2.1, RFC 6749 §10.12
nonce Copied by the OP into the ID token, binding it to this browser session. Core §3.1.2.1, §15.5.2
code_challenge, code_challenge_method=S256 PKCE: proves the party redeeming the code started the request. RFC 7636 §4.2–4.3
prompt, max_age, acr_values (optional) Force re-login, cap authentication age, request an assurance level. Core §3.1.2.1

Step 3: the callback

HTTP/1.1 302 Found
Location: https://app.example.com/callback?code=SplxlOBeZQQYbYS6WxSbIA-example
  &state=af0ifjsldkj-example
  &iss=https%3A%2F%2Flogin.example.com

The code is single-use and short-lived; RFC 6749 recommends a maximum lifetime of 10 minutes (§4.1.2). The RP checks state against the session (Core §3.1.2.7) and iss against the expected issuer (RFC 9207 §2.4) before doing anything else.

Step 4: the token request

POST /oauth/token HTTP/1.1
Host: login.example.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&code=SplxlOBeZQQYbYS6WxSbIA-example
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk
&client_id=s6BhdRkqt3-example
&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer
&client_assertion=eyJhbGciOiJQUzI1NiIsImtpZCI6ImNsaWVudC1rZXktMSJ9.eyJpc3MiOiJzNkJoZFJrcXQzLWV4YW1wbGUiLCJhdWQiOiJodHRwczovL2xvZ2luLmV4YW1wbGUuY29tIn0.c2lnbmF0dXJlLWV4YW1wbGU
Parameter Meaning Spec
grant_type=authorization_code, code Redeem the code from step 3. RFC 6749 §4.1.3
redirect_uri Must be identical to the one in step 1 if it was sent there. RFC 6749 §4.1.3
code_verifier The server hashes it and compares to the stored challenge. RFC 7636 §4.5–4.6
client_assertion (private_key_jwt) A short JWT signed by the client's private key. Its aud must identify this authorization server; the 2025 audience findings in the mistakes table show what happens when it is loose. RFC 7523 §2.2, §3, Core §9

The example PKCE values are the illustrative ones from RFC 7636 Appendix B. Never reuse them.

Step 5: the token response and the ID token

{
  "access_token": "SlAV32hkKG-example",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "8xLOxBtZp8-example",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjIwMjYtMTAta2V5LWEiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOi...signature"
}

The response must carry Cache-Control: no-store (Core §3.1.3.3). Decoded, the ID token header and payload look like this:

{ "alg": "RS256", "kid": "2026-10-key-a", "typ": "JWT" }
{
  "iss": "https://login.example.com",
  "sub": "248289761001-example",
  "aud": "s6BhdRkqt3-example",
  "exp": 1793887631,
  "iat": 1793887331,
  "auth_time": 1793887322,
  "nonce": "n-0S6_WzA2Mj-example",
  "acr": "phr",
  "amr": ["hwk", "user"],
  "at_hash": "77QmUPtjPfzWtF2AnpK9RQ",
  "email": "ada@example.com",
  "email_verified": true
}
Claim Meaning Spec
iss Issuer identifier, compared by exact string match. Core §2
sub Stable, never-reassigned identifier within this issuer. The account key is iss + sub. Core §2, §5.7
aud Must contain your client_id. Can be an array. Core §2
exp, iat Expiry and issue time, seconds since the epoch. Core §2
auth_time When the user actually authenticated; required if you sent max_age. Core §2
nonce Echo of your request value. Core §2
acr, amr Assurance class and methods used. Values are deployment-specific; amr values are registered in RFC 8176. Core §2
at_hash Left half of the hash of the access token, base64url. Lets you confirm the access token came with this ID token. Core §3.1.3.6
email, email_verified Profile claims. Display them; do not key accounts on them. Core §5.1

Step 6: userinfo (optional)

GET /userinfo HTTP/1.1
Host: login.example.com
Authorization: Bearer SlAV32hkKG-example

The sub in the userinfo response must exactly match the sub in the ID token, or the RP must not use the response (Core §5.3.2).

The checks a relying party must run

  1. state matches the value in the user's pre-login session, then discard it (Core §3.1.2.7).
  2. iss on the redirect equals the expected issuer when the OP supports RFC 9207 (RFC 9207 §2.4).
  3. ID token iss exactly equals the discovered issuer (Core §3.1.3.7).
  4. aud contains your client_id and no audiences you do not trust; if azp is present, it is your client_id (Core §3.1.3.7).
  5. Signature verifies with a key from the issuer's JWKS, using the algorithm you registered. Reject alg: none, reject symmetric algorithms you did not configure, and never take the key from the token header (Core §3.1.3.7, §10.1).
  6. exp is in the future and iat is within your acceptable window (Core §3.1.3.7).
  7. nonce is present and equals the value you sent (Core §3.1.3.7).
  8. acr and auth_time satisfy what you requested, if you requested them (Core §3.1.3.7).

The JWT layer underneath has its own rules: RFC 7519 §7.2 for validating a JWT, and the JWT best current practices in RFC 8725 for algorithm allow-lists.

Common implementation mistakes, with the CVEs they produced

Mistake What goes wrong Real examples
Accepting unsigned tokens or alg: none Anyone can mint an ID token CVE-2026-48558 (SimpleHelp, exploited), CVE-2023-29357 (SharePoint)
Accepting algorithms you never configured, or picking the key from attacker input Key confusion; HS256 verified with a public key or a looked-up secret CVE-2026-5430 (WSO2), CVE-2025-65945 (node-jws), CVE-2025-9485
Loose aud on private_key_jwt client assertions An assertion meant for one server is replayed at another CVE-2025-27370, CVE-2025-27371, technique: token replay at an unbound endpoint
Expired or retired keys still trusted A key you rotated out keeps working CVE-2025-31123 (Zitadel)
Re-authentication not bound to the original user prompt=login or account linking merges the wrong session CVE-2023-6787, CVE-2025-7365 (Keycloak)
Accepting any tenant of a multi-tenant issuer A user from any tenant signs in cross-tenant token confusion
Keying accounts on email Account takeover through an unverified or reassigned email Pattern; use iss + sub
Calling APIs with the ID token The API accepts a token whose audience is the client Send the access token; see the OAuth walkthrough

None of these checks stop a user who completes a real login on a phishing proxy; the proxy steals the resulting session. That is fixed at the OP with phishing-resistant authentication, see how WebAuthn works, message by message and AiTM phishing.

Try it

Run your provider through the OIDC discovery checker to confirm the issuer, JWKS and supported algorithms, then paste an ID token into the JWT decoder and compare each claim to the table above. The validate a JWT recipe and the authorization code with PKCE recipe turn the checklist into code.

OpenID Connect standard page, OAuth vs OIDC, SAML message flow, implementing OAuth 2.0 and OIDC. Glossary: ID token, JWT, JWKS, PKCE, relying party, claims.

Frequently asked questions

How does OIDC work?
OpenID Connect runs on top of the OAuth 2.0 authorization code flow. The relying party reads the provider's discovery document, redirects the browser to the authorization endpoint with scope=openid, a state, a nonce and a PKCE challenge, receives a one-time code on its redirect URI, and exchanges that code plus the PKCE verifier at the token endpoint. The token response carries an ID token, a signed JWT that says who logged in, when and how, plus an access token for the userinfo endpoint or APIs.
What is the difference between OIDC and OAuth 2.0 in the flow?
The messages are the same OAuth authorization code flow. OIDC adds the openid scope, the nonce parameter, the ID token in the token response, the userinfo endpoint and the discovery document. OAuth alone gives the client an access token for an API; OIDC adds a verifiable statement about the user's authentication that the client itself consumes.
Which ID token claims must be validated?
OIDC Core section 3.1.3.7 requires the client to check that iss exactly matches the issuer, that aud contains its client_id and no untrusted audiences, that the signature verifies with the issuer's keys using the expected algorithm, that exp is in the future, and that nonce matches the value it sent. iat, acr and auth_time should be checked when relevant.
Do I need both state and nonce if I use PKCE?
PKCE protects the code exchange. state protects the redirect against cross-site request forgery and carries your own session context, and nonce binds the ID token to the browser session that started the login. RFC 9700 accepts PKCE as the primary defense against code injection, but most libraries send all three, and OIDC Core requires a nonce match whenever a nonce was sent.
Last reviewed By SWI Community TeamSuggest a correctionHow we research