# How OpenID Connect Works, Message by Message

Source: https://startwithidentity.com/guides/protocols/oidc-message-flow/
Last updated: 2026-11-05
License: content by Start with Identity. Cite the source URL.

---

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](https://openid.net/specs/openid-connect-core-1_0.html) 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](https://startwithidentity.com/standards/openid-connect/). For the OAuth layer underneath, including client credentials and refresh, see [how OAuth 2.0 and 2.1 work, message by message](https://startwithidentity.com/guides/protocols/oauth-message-flow/).

## The sequence at a glance

| # | From → to | Message | Spec |
| --- | --- | --- | --- |
| 0 | RP → OP | `GET /.well-known/openid-configuration` | [Discovery §4](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfig) |
| 1 | RP → browser → OP | Authentication request to the authorization endpoint | [Core §3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) |
| 2 | OP ↔ user | Login, MFA, consent | [Core §3.1.2.3–3.1.2.4](https://openid.net/specs/openid-connect-core-1_0.html#Authenticates) |
| 3 | OP → browser → RP | Redirect with `code`, `state`, `iss` | [Core §3.1.2.5](https://openid.net/specs/openid-connect-core-1_0.html#AuthResponse), [RFC 9207 §2](https://www.rfc-editor.org/rfc/rfc9207#section-2) |
| 4 | RP → OP (back channel) | Token request with `code` and `code_verifier` | [Core §3.1.3.1](https://openid.net/specs/openid-connect-core-1_0.html#TokenRequest), [RFC 7636 §4.5](https://www.rfc-editor.org/rfc/rfc7636#section-4.5) |
| 5 | OP → RP | Token response: `id_token`, `access_token`, optional `refresh_token` | [Core §3.1.3.3](https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse) |
| 6 | RP → OP | Optional userinfo request with the access token | [Core §5.3](https://openid.net/specs/openid-connect-core-1_0.html#UserInfo) |

## Step 0: discovery

```http
GET /.well-known/openid-configuration HTTP/1.1
Host: login.example.com
```

```json
{
  "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](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderConfigurationValidation) |
| `jwks_uri` | Where the signing keys live. Cache with a TTL and refetch on an unknown `kid`. | [Discovery §3](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata) |
| `id_token_signing_alg_values_supported` | What the OP may sign with. Your RP should accept only the one algorithm you registered. | [Discovery §3](https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata) |
| `code_challenge_methods_supported` | Whether PKCE `S256` is available (it should be). | [RFC 8414 §2](https://www.rfc-editor.org/rfc/rfc8414#section-2) |
| `authorization_response_iss_parameter_supported` | The OP adds `iss` to the redirect, which defeats mix-up attacks. | [RFC 9207 §3](https://www.rfc-editor.org/rfc/rfc9207#section-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](https://www.rfc-editor.org/rfc/rfc7636#section-4.1)). It sends the SHA-256 of the verifier as the challenge.

```http
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](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) |
| `scope=openid ...` | `openid` is what makes this OIDC; `profile` and `email` request standard claims. | [Core §3.1.2.1, §5.4](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims) |
| `client_id` | The RP's registered identifier; becomes the ID token `aud`. | [RFC 6749 §2.2](https://www.rfc-editor.org/rfc/rfc6749#section-2.2) |
| `redirect_uri` | Must exactly match a registered value. | [Core §3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest), [RFC 9700 §2.1](https://www.rfc-editor.org/rfc/rfc9700#section-2.1) |
| `state` | Opaque CSRF token you check on return. | [Core §3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest), [RFC 6749 §10.12](https://www.rfc-editor.org/rfc/rfc6749#section-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](https://openid.net/specs/openid-connect-core-1_0.html#NonceNotes) |
| `code_challenge`, `code_challenge_method=S256` | PKCE: proves the party redeeming the code started the request. | [RFC 7636 §4.2–4.3](https://www.rfc-editor.org/rfc/rfc7636#section-4.3) |
| `prompt`, `max_age`, `acr_values` (optional) | Force re-login, cap authentication age, request an assurance level. | [Core §3.1.2.1](https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest) |

## Step 3: the callback

```http
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](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.2)). The RP checks `state` against the session ([Core §3.1.2.7](https://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation)) and `iss` against the expected issuer ([RFC 9207 §2.4](https://www.rfc-editor.org/rfc/rfc9207#section-2.4)) before doing anything else.

## Step 4: the token request

```http
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](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3) |
| `redirect_uri` | Must be identical to the one in step 1 if it was sent there. | [RFC 6749 §4.1.3](https://www.rfc-editor.org/rfc/rfc6749#section-4.1.3) |
| `code_verifier` | The server hashes it and compares to the stored challenge. | [RFC 7636 §4.5–4.6](https://www.rfc-editor.org/rfc/rfc7636#section-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](https://www.rfc-editor.org/rfc/rfc7523#section-3), [Core §9](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication) |

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

```json
{
  "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](https://openid.net/specs/openid-connect-core-1_0.html#TokenResponse)). Decoded, the ID token header and payload look like this:

```json
{ "alg": "RS256", "kid": "2026-10-key-a", "typ": "JWT" }
```

```json
{
  "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](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) |
| `sub` | Stable, never-reassigned identifier within this issuer. The account key is `iss` + `sub`. | [Core §2, §5.7](https://openid.net/specs/openid-connect-core-1_0.html#ClaimStability) |
| `aud` | Must contain your `client_id`. Can be an array. | [Core §2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) |
| `exp`, `iat` | Expiry and issue time, seconds since the epoch. | [Core §2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) |
| `auth_time` | When the user actually authenticated; required if you sent `max_age`. | [Core §2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) |
| `nonce` | Echo of your request value. | [Core §2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) |
| `acr`, `amr` | Assurance class and methods used. Values are deployment-specific; `amr` values are registered in [RFC 8176](https://www.rfc-editor.org/rfc/rfc8176). | [Core §2](https://openid.net/specs/openid-connect-core-1_0.html#IDToken) |
| `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](https://openid.net/specs/openid-connect-core-1_0.html#CodeIDToken) |
| `email`, `email_verified` | Profile claims. Display them; do not key accounts on them. | [Core §5.1](https://openid.net/specs/openid-connect-core-1_0.html#StandardClaims) |

## Step 6: userinfo (optional)

```http
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](https://openid.net/specs/openid-connect-core-1_0.html#UserInfoResponse)).

## 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](https://openid.net/specs/openid-connect-core-1_0.html#AuthResponseValidation)).
2. **`iss` on the redirect** equals the expected issuer when the OP supports RFC 9207 ([RFC 9207 §2.4](https://www.rfc-editor.org/rfc/rfc9207#section-2.4)).
3. **ID token `iss`** exactly equals the discovered `issuer` ([Core §3.1.3.7](https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation)).
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](https://openid.net/specs/openid-connect-core-1_0.html#Signing)).
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](https://www.rfc-editor.org/rfc/rfc7519#section-7.2) for validating a JWT, and the [JWT best current practices in RFC 8725](https://www.rfc-editor.org/rfc/rfc8725) 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](https://startwithidentity.com/cves/cve-2026-48558/) (SimpleHelp, exploited), [CVE-2023-29357](https://startwithidentity.com/cves/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](https://startwithidentity.com/cves/cve-2026-5430/) (WSO2), [CVE-2025-65945](https://startwithidentity.com/cves/cve-2025-65945/) (node-jws), [CVE-2025-9485](https://startwithidentity.com/cves/cve-2025-9485/) |
| Loose `aud` on `private_key_jwt` client assertions | An assertion meant for one server is replayed at another | [CVE-2025-27370](https://startwithidentity.com/cves/cve-2025-27370/), [CVE-2025-27371](https://startwithidentity.com/cves/cve-2025-27371/), technique: [token replay at an unbound endpoint](https://startwithidentity.com/techniques/token-replay-unbound-endpoint/) |
| Expired or retired keys still trusted | A key you rotated out keeps working | [CVE-2025-31123](https://startwithidentity.com/cves/cve-2025-31123/) (Zitadel) |
| Re-authentication not bound to the original user | `prompt=login` or account linking merges the wrong session | [CVE-2023-6787](https://startwithidentity.com/cves/cve-2023-6787/), [CVE-2025-7365](https://startwithidentity.com/cves/cve-2025-7365/) (Keycloak) |
| Accepting any tenant of a multi-tenant issuer | A user from any tenant signs in | [cross-tenant token confusion](https://startwithidentity.com/techniques/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](https://startwithidentity.com/guides/protocols/oauth-message-flow/) |

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](https://startwithidentity.com/guides/protocols/webauthn-message-flow/) and [AiTM phishing](https://startwithidentity.com/techniques/aitm-phishing-proxy/).

## Try it

Run your provider through the [OIDC discovery checker](https://startwithidentity.com/tools/oidc-discovery-checker/) to confirm the `issuer`, JWKS and supported algorithms, then paste an ID token into the [JWT decoder](https://startwithidentity.com/tools/jwt-decoder/) and compare each claim to the table above. The [validate a JWT recipe](https://startwithidentity.com/recipes/validate-a-jwt/) and the [authorization code with PKCE recipe](https://startwithidentity.com/recipes/oidc-authorization-code-pkce/) turn the checklist into code.

## Related

[OpenID Connect standard page](https://startwithidentity.com/standards/openid-connect/), [OAuth vs OIDC](https://startwithidentity.com/guides/fundamentals/oauth-vs-oidc/), [SAML message flow](https://startwithidentity.com/guides/protocols/saml-message-flow/), [implementing OAuth 2.0 and OIDC](https://startwithidentity.com/guides/implementation/oauth-2-openid-connect-implementation/). Glossary: [ID token](https://startwithidentity.com/glossary/id-token/), [JWT](https://startwithidentity.com/glossary/jwt/), [JWKS](https://startwithidentity.com/glossary/jwks/), [PKCE](https://startwithidentity.com/glossary/pkce/), [relying party](https://startwithidentity.com/glossary/relying-party/), [claims](https://startwithidentity.com/glossary/claims/).
