A mobile app signs a user in with an identity provider and passes the access token to your backend, which calls the provider's profile endpoint, gets an email address and creates a session. Every test passes. It also lets any other app the user signed in to with that provider log in to your app as them with its own access token, because nothing checks which app the token was issued to.
OpenID Connect (OIDC) closes that gap. It adds an ID token to OAuth 2.0: a signed JWT in which the provider states who the user is, which app the token is for and when the user signed in.
This post draws the authorization code flow with PKCE as a sequence diagram and covers what OIDC adds: the openid scope, nonce, ID token validation, discovery, key rotation, UserInfo and logout.
OAuth 2.0 vs OpenID Connect
OAuth 2.0 is about access: a token to call an API on the user's behalf. OpenID Connect is about identity: who signed in.
| OAuth 2.0 | OpenID Connect | |
|---|---|---|
| Question it answers | May this app call this API? | Who is the user, and when did they sign in? |
| Token | Access token | ID token |
| Token is meant for | The API | Your app |
| Token format | Not defined: opaque or JWT | A signed JWT |
| How you ask | Scopes the API defines | scope includes openid |
| Defines | Grants and endpoints | Claims, UserInfo, discovery, logout |
On the wire, an OIDC sign-in is an OAuth authorization request with openid in the scope, and the token response carries an id_token next to the access token. The OAuth 2.0 flow diagram covers the OAuth half in detail.
The flow, step by step
OpenID Connect Core 1.0 calls your app the relying party (RP) and the authorization server the OpenID provider (OP). What passes through the browser, the front channel, can leak into URLs, history and logs; calls from your backend to the provider, the back channel, cannot. Numbers match the diagram.
- Start (1–3). The user clicks Sign in. Your app stores a fresh
state,nonceand PKCEcode_verifierin the session and redirects the browser to the provider. - Authentication request (4). The browser follows the redirect:
GET /authorize?response_type=code
&client_id=orders-web
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20profile%20email
&state=7fJq2LbX9aQe
&nonce=n-0S6_WzA2Mj
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256 HTTP/1.1
Host: op.example.com
- Login and consent (5). The provider authenticates the user (password, passkey, MFA or an existing session) and asks for consent. Your app never sees credentials.
- Code back (6–7). The provider redirects to the registered
redirect_uriwith a short-livedcodeand the samestate, which your app compares with the session. - Token request (8–9). Your backend posts the
code,redirect_uriandcode_verifierwith its client credentials. The provider checks the verifier against the challenge and returns anid_token, anaccess_tokenand often arefresh_token. - Validation (10). Your app validates the ID token, fetching signing keys only when the one it names is not cached.
- UserInfo (11–12), optional. Only when the ID token lacks claims you need.
- Session (13). Your app creates its own session and sets a cookie.
What state, nonce and PKCE protect
| Value | Travels in | Checked by | Stops |
|---|---|---|---|
state |
Step 4 and the redirect back | Your app | A login response the user never started (CSRF) |
nonce |
Step 4, then inside the ID token | Your app | ID token replay, and code injection for confidential clients |
code_verifier |
Its SHA-256 hash in step 4, itself in step 8 | The provider | Redeeming a stolen code |
The OAuth security best current practice, RFC 9700, lets one stand in for another in some cases, but each costs one random value, so send all three. Public clients such as single-page and mobile apps must use PKCE (RFC 7636) with S256.
Inside the ID token
The ID token from step 9, decoded:
{
"iss": "https://op.example.com",
"sub": "248289761001",
"aud": "orders-web",
"nonce": "n-0S6_WzA2Mj",
"iat": 1793736400,
"exp": 1793740000,
"auth_time": 1793736390
}
| Claim | Meaning | Your app |
|---|---|---|
iss |
Issuer, an https URL | Must equal the discovered issuer exactly |
sub |
User ID, never reassigned within the issuer, at most 255 ASCII characters | Uses iss plus sub as the account key |
aud |
Audience, a string or an array | Must find its client_id in it |
exp, iat |
Expiry and issue time, in Unix seconds | Rejects expired or very old tokens |
nonce |
The value sent in step 4 | Must match the session |
auth_time |
When the user actually authenticated | Checks it when you send max_age |
acr, amr |
Assurance level and methods, such as pwd or mfa |
Checks them before sensitive actions |
Core is explicit that only iss plus sub identifies a user: email addresses change and can be reassigned.
Validating the ID token
Core section 3.1.3.7 lists the rules:
- Pin the algorithm (RS256 by default) instead of trusting the
algheader, and never acceptnone. - Verify the signature with the JWKS key named by
kid. Core lets a code-flow client rely on TLS instead, since the token came straight from the token endpoint, but verifying costs little. - Check
issagainst the discovered issuer, character for character. - Check
audfor yourclient_id, and reject audiences you do not trust. - Check
expandiat, allowing seconds to a few minutes of clock skew. - Check
nonceagainst the session, then delete it. - Check
auth_timeandacrif you requested them.
Only then create a session. With the jose library in Node.js:
import { createRemoteJWKSet, jwtVerify } from 'jose'
// issuer and jwks_uri come from the discovery document.
// The key set is cached; an unknown kid triggers a refetch
// (at most once every 30 seconds by default).
const JWKS = createRemoteJWKSet(new URL(oidc.jwks_uri))
const { payload } = await jwtVerify(tokens.id_token, JWKS, {
issuer: oidc.issuer, // exact match
audience: CLIENT_ID, // must appear in aud
algorithms: ['RS256'], // never trust the header alone
clockTolerance: 60, // seconds of clock skew
maxTokenAge: 600, // rejects an old iat
})
if (payload.nonce !== session.nonce) throw new Error('nonce mismatch')
Discovery and key rotation
OpenID Connect Discovery 1.0 puts a provider's settings in one JSON document at the issuer URL plus /.well-known/openid-configuration. Google's, trimmed to the fields this flow uses, at the time of writing (November 2026):
{
"issuer": "https://accounts.google.com",
"authorization_endpoint": "https://accounts.google.com/o/oauth2/v2/auth",
"token_endpoint": "https://oauth2.googleapis.com/token",
"userinfo_endpoint": "https://openidconnect.googleapis.com/v1/userinfo",
"jwks_uri": "https://www.googleapis.com/oauth2/v3/certs",
"id_token_signing_alg_values_supported": ["RS256"],
"code_challenge_methods_supported": ["plain", "S256"]
}
The issuer must be identical to the issuer URL you fetched it from and to the iss of every ID token. Configure the expected issuer yourself; never take it from a token you have not verified.
Keys rotate through the jwks_uri: the provider adds a new key, signs with it and keeps the old one published for a while (Core section 10.1.1). Cache the set, honor Cache-Control, refetch on an unfamiliar kid and rate-limit those refetches. The JWT authentication flow diagram covers the same mechanics for access tokens.
UserInfo
UserInfo returns claims for the granted scopes (profile, email, address, phone) when you call it with the access token. Its response always contains sub, which must exactly match the ID token's sub, or you discard the response.
Logout options
Three final OpenID specifications cover signing out:
| Specification | How it works | Watch out for |
|---|---|---|
| RP-Initiated Logout 1.0 | Your app redirects to the provider's end_session_endpoint with id_token_hint |
Your app must also end its own session |
| Front-Channel Logout 1.0 | The provider loads each app's logout URL in an iframe | Third-party cookie blocking can break it |
| Back-Channel Logout 1.0 | The provider POSTs a signed logout token to each app | Your app must find and end the session server-side |
Core also notes that the ID token's exp is unrelated to the session lifetime.
Mistakes that show up in reviews
- An access token as proof of identity. The opening scene. Sign users in only with an ID token your backend got from the token endpoint and validated.
- Skipping
audornonce. Every app registered at the same provider gets validly signed ID tokens with the sameiss; onlyaudsays a token was issued to you, and onlynonceties it to this login. - The implicit flow.
response_type=id_token tokenputs tokens in the URL fragment; RFC 9700 says clients should not use response types that return access tokens in the authorization response. Usecodewith PKCE, also in single-page apps. - ID tokens sent to APIs. APIs should accept only access tokens issued for them; the API gateway architecture diagram shows where that check usually sits.
The OpenID Connect flow diagram
Diagram contents
- User
- Browser
- Relying party (your app)
- OpenID provider
- all checks must pass
- claims missing from the ID token
- ID token: for your app (aud = client_id)
- Access token: for UserInfo and APIs
- 1. Click Sign in
- 2. GET /login
- 3. 302 to the provider's /authorize
- 4. GET /authorize: scope=openid, state, nonce, code_challenge
- 5. Log in (password, passkey, MFA) and consent
- 6. 302 to redirect_uri with code, state
- 7. GET /callback?code&state
- 8. POST /token: code, code_verifier, client auth
- 9. id_token, access_token
- 10. GET jwks_uri (when kid is not cached)
- 11. GET /userinfo, Bearer access_token
- 12. Claims; sub must match the ID token
- 13. 302 to the app, Set-Cookie: session
How to read it:
- Four participants: the user, the browser, your app (the relying party) and the OpenID provider. Time runs downward.
- Colors show the channel: blue through the browser, teal from your backend to the provider, violet for the user's actions. Dashed arrows are responses and redirects.
- Steps 3 and 4 carry
scope=openid,state,nonceand the PKCE challenge; step 8 carries the verifier. - The
criticalfragment holds the key fetch and the checks that must pass before a session exists. Theoptfragment is the UserInfo call. - The right column maps discovery fields to steps, lists PKCE parameters, decodes an ID token and shows which token is for whom.
Drawing OpenID Connect diagrams clearly
- Give the browser its own lifeline. The split between front and back channel is the security story of the flow.
- Write the checked parameters on the arrows:
state,nonce,code_challenge,code_verifier. - Spell out the validation. "Validate token" cannot be reviewed; "signature, iss, aud, exp, iat, nonce" can.
- Say which token goes where, with its audience. Many OIDC bugs are a token sent to the wrong party.
- One flow per diagram. Draw login, refresh and logout separately, with placeholder IDs and tokens.
The sequence diagram example covers the notation; the SAML SSO flow diagram shows the XML-based protocol enterprise customers still ask for.
Build it in Archboard
- Sign in with Google or GitHub and create a diagram.
- From the Sequence pack, add an Actor for the user and three Lifeline nodes of equal height: Browser, Relying party (your app) and OpenID provider.
- Draw the 13 messages as straight, numbered edges, top to bottom. Make responses dashed and color the front and back channels differently.
- Add a Fragment from the Sequence pack with the operator
criticalaround the key fetch and checks, and a second Fragment withoptaround the UserInfo call. - Add Text from the Core pack beside the relying party for the session values and checks, plus a color legend.
- Add a Markdown block from the Core pack for discovery, PKCE and a decoded ID token, and two Token nodes from the Security pack for the ID token and the access token.
The example uses 9 diagram nodes, within the Free plan. Your AI agent can also draw it over MCP.
FAQ
What is the difference between OAuth 2.0 and OpenID Connect?
OAuth 2.0 gives an app an access token to call an API. OpenID Connect adds sign-in on top: the openid scope, an ID token that says who the user is, and standard discovery and UserInfo endpoints.
Is an ID token the same as an access token? No. The ID token is a signed JWT for your app that says who signed in. The access token is for APIs, including UserInfo. Never send ID tokens to APIs or accept access tokens as proof of login.
Do I need PKCE if I already send a nonce? Use both. RFC 9700 requires PKCE for public clients. A confidential client may rely on the nonce with extra precautions, but PKCE stops a stolen code from being redeemed, while the nonce only detects an injected code after the token response.
How do I validate an ID token?
Verify the signature with the JWKS key matching its kid, then check that iss equals the issuer, aud contains your client ID, exp has not passed, iat is recent and nonce matches the session.
What is /.well-known/openid-configuration?
The OpenID Connect discovery document. It lists a provider's issuer, endpoints, algorithms and jwks_uri, so your app can configure itself from one URL.