OpenID Connect: the identity layer on top of OAuth
How OIDC turns OAuth 2.0 into real authentication — the ID token (JWT) vs the access token, standard claims (sub, iss, aud, exp, email…), the discovery (.well-known/openid-configuration) and UserInfo endpoints, JWKS validation, the Authorization Code/Hybrid/Implicit flows, and front-channel, back-channel, and RP-initiated logout.
The layer OAuth was missing
The previous article ended on a warning: OAuth 2.0 authorizes a client but does not authenticate the user, and apps that used an access token as a login created real security holes. OpenID Connect (OIDC) is the fix the industry standardized — a thin identity layer built on top of OAuth 2.0, published by the OpenID Foundation in 2014, that adds proper authentication without throwing away any of OAuth’s machinery.
The genius of OIDC is how little it adds. It reuses OAuth’s flows, endpoints, and access tokens unchanged, and bolts on exactly the pieces OAuth deliberately omitted: a token that asserts who the user is (the ID token), a place to fetch more about them (the UserInfo endpoint), a standard vocabulary for user attributes (claims), and a way for a client to configure itself automatically (discovery). That’s it. When you click “Sign in with Google,” you are using OIDC, which is using OAuth underneath.
That standardization is the whole point. Before OIDC, every provider that offered “social login” did it with a proprietary, slightly different OAuth extension, and developers wrote bespoke integration code for each one. OIDC replaced that with one ID-token format, one discovery document, and one validation procedure that behaves identically against Google, Microsoft, Okta, Auth0, or any compliant provider. Learn it once, integrate anywhere — which is exactly why it spread so fast.
ID token vs access token: the central distinction
The heart of OIDC is a second token alongside OAuth’s access token, with a completely different purpose and audience. Getting this pair straight is most of understanding OIDC.
flowchart TD accTitle: ID token versus access token in OpenID Connect accDescr: The authorization server issues two tokens. The ID token is about identity, is intended for the client, is a JWT the client reads and validates to authenticate the user. The access token is about authorization, is intended for the resource server, and the client should treat it as opaque and only forward it to the API. The client uses the ID token to log the user in and the access token to call protected APIs. AS[Authorization Server] --> IDT[ID token<br/>identity — for the CLIENT] AS --> AT[Access token<br/>authorization — for the API] IDT --> C[Client reads & validates it<br/>to log the user in] AT --> RS[Client forwards it<br/>to call the resource server]
- The ID token is about identity and is for the client. It’s a signed JWT the client opens, validates, and reads to establish who just logged in. Its audience (
aud) is the client itself. - The access token is about authorization and is for the resource server. The client should treat it as opaque — it doesn’t read it, it just forwards it to APIs. Its audience is the resource server.
The classic mistake is using the access token to identify the user (it isn’t meant for that, and may not even be a JWT you can read) or sending the ID token to an API as if it were an access token (the API isn’t its audience). Right tool, right job: ID token to know who, access token to call what.
The ID token: a JWT full of claims
The ID token is a JWT (JSON Web Token) — three base64url parts (header, payload, signature) the client can decode and verify. Its payload is a set of claims about the authentication event and the user. A decoded payload looks like this:
{
"iss": "https://accounts.example.com",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"exp": 1718000300,
"iat": 1718000000,
"auth_time": 1717999990,
"nonce": "n-0S6_WzA2Mj",
"email": "sara@example.com",
"email_verified": true,
"given_name": "Sara",
"family_name": "Lopez"
}The claims fall into two groups. The protocol claims make the token trustworthy:
iss— the issuer (which provider minted this). The client must recognize it.sub— the subject: a stable, unique identifier for the user at this issuer. This, not email, is the real primary key — emails change,subdoesn’t.aud— the audience: the client ID this token was minted for. The client must confirm it’s the intended audience.exp/iat— expiry and issued-at timestamps that bound the token’s validity.auth_time/nonce— when the user actually authenticated, and a value the client generated to bind the token to its request and block replay.
The identity claims describe the user — email, email_verified, given_name, family_name, name, picture, and so on — populated according to the scopes requested. OIDC ties claims to scopes so the client receives only what it asks for:
| Scope | Claims it releases |
|---|---|
openid | sub (required — this scope is what makes the request OIDC at all) |
profile | name, given_name, family_name, picture, locale, … |
email | email, email_verified |
address | address |
phone | phone_number, phone_number_verified |
Always request openid (it’s mandatory and is what triggers the ID token), then add profile or email only if you genuinely need them. Claim minimization is the identity-layer echo of OAuth’s scope minimization: don’t ask for attributes you won’t use.
Discovery, JWKS, and UserInfo
OIDC standardizes how a client learns about a provider and how it gets the keys to validate tokens — which is what makes “add a new identity provider” a configuration task rather than a coding project.
- Discovery — every compliant provider publishes a JSON document at
/.well-known/openid-configurationlisting itsauthorization_endpoint,token_endpoint,userinfo_endpoint, thejwks_uri, and the scopes, claims, and algorithms it supports. A client points at the issuer URL and configures itself. - JWKS — the
jwks_uriserves the provider’s public signing keys (a JSON Web Key Set). The client fetches them to verify ID-token signatures, and because keys are fetched bykid(key ID), the provider can rotate signing keys without breaking anyone. - UserInfo — an OAuth-protected endpoint the client calls with the access token to retrieve additional claims about the user, for when you want profile data beyond what’s in the ID token.
sequenceDiagram accTitle: OIDC discovery and ID token validation accDescr: The client fetches the discovery document from the provider's well-known openid-configuration URL and receives the endpoint URLs and the JWKS URI. The client then fetches the JSON Web Key Set from the JWKS URI to obtain the provider's public signing keys. When the client later receives an ID token, it selects the key by its key ID and verifies the token's signature, issuer, audience, and expiry before trusting its claims. participant C as Client (RP) participant P as OIDC Provider C->>P: GET /.well-known/openid-configuration P->>C: Endpoints + jwks_uri C->>P: GET jwks_uri P->>C: Public signing keys (JWKS) Note over C: Later: validate ID token signature by kid,<br/>then check iss, aud, exp, nonce
The flows
OIDC uses OAuth’s grant types, specialized for authentication by adding the openid scope (which is what tells the provider to also issue an ID token).
- Authorization Code (with PKCE) — the standard, recommended flow for essentially everything: web apps, SPAs, and mobile. The client gets a code via redirect and exchanges it for an ID token and access token. This is the OIDC flow you should use.
- Hybrid — returns some tokens directly from the authorization endpoint and others from the token endpoint. It exists for specific scenarios but adds complexity and front-channel token exposure; most teams don’t need it.
- Implicit — returned tokens directly in the redirect, like OAuth’s implicit grant. Deprecated for the same reasons: tokens leak into browser history and logs. Authorization Code + PKCE replaces it everywhere, including SPAs.
The Authorization Code flow in OIDC
The flow is OAuth’s Authorization Code + PKCE, with one addition — the openid scope — that makes the provider return an ID token:
sequenceDiagram accTitle: OpenID Connect Authorization Code flow accDescr: The client redirects the user to the OIDC provider's authorization endpoint requesting the openid scope plus a PKCE code challenge and a nonce. The provider authenticates the user and returns a one-time authorization code. The client exchanges the code and PKCE verifier at the token endpoint and receives both an ID token and an access token. The client validates the ID token to authenticate the user, then optionally calls the UserInfo endpoint with the access token for more claims. actor U as User participant C as Client (RP) participant P as OIDC Provider C->>P: Authorize (scope=openid, code_challenge, nonce) P->>U: Authenticate + consent P->>C: Authorization code (one-time) C->>P: Exchange code + code_verifier P->>C: ID token + access token C->>C: Validate ID token (sig, iss, aud, exp, nonce) C->>P: (optional) UserInfo with access token P->>C: Additional claims
The single conceptual difference from plain OAuth is the ID token coming back and the client validating it to authenticate. Everything else — redirects, PKCE, the code exchange — is OAuth you already know. That reuse is exactly why OIDC won: it gave developers authentication for nearly free on top of an authorization framework they were already deploying.
From ID token to a session
A subtlety that trips up newcomers: the ID token is proof that a login happened, at a specific moment — it is not a long-lived session token, and it is not meant to be sent to your own APIs on every request. Its job is to be validated once, right after login, so the client can establish who the user is.
What happens next is the client’s responsibility. After validating the ID token, the app mints its own session — typically a secure, http-only session cookie — and that session, not the ID token, carries the user through subsequent requests. The ID token has done its job and can be discarded.
The two tokens then diverge cleanly in their later life:
- Need to keep calling APIs as the user? That’s authorization — use the access token, and renew it with the OAuth refresh token when it expires. The ID token plays no part in API calls.
- Need to keep the user logged in to your app? That’s your session’s job, governed by your own timeout policy, independent of the tokens.
A frequent bug is shipping the ID token to a backend as a bearer credential, conflating “who logged in” with “may call this API.” Keep them separate: the ID token authenticates the login, the access token authorizes the calls, and your session cookie maintains continuity. This separation also explains why logout is its own problem — ending your session is easy; ending the provider’s session and every other app’s is the hard part below.
Logout: three mechanisms, one hard problem
Logging in is the easy half; logging out cleanly across many apps that share one provider is genuinely hard — the same lesson SAML’s Single Logout taught. OIDC defines three mechanisms:
flowchart TD accTitle: OIDC logout mechanisms accDescr: OIDC defines three logout mechanisms. RP-initiated logout has the relying party redirect the user to the provider's end-session endpoint to terminate the central session. Front-channel logout uses the browser to load hidden logout URLs at each relying party. Back-channel logout has the provider make direct server-to-server calls to each relying party's logout endpoint. Back-channel is the most reliable because it does not depend on the browser. L[User logs out] --> RP[RP-initiated<br/>redirect to provider end_session] RP --> FC[Front-channel<br/>browser loads hidden logout URLs] RP --> BC[Back-channel<br/>provider calls each app server-to-server] FC --> R[Sessions terminated<br/>best-effort] BC --> R
- RP-initiated logout: the relying party (app) redirects the user to the provider’s
end_session_endpoint, ending the central session at the provider. This is the baseline and the most widely supported. - Front-channel logout: the provider’s logout page loads hidden iframes pointing at each app’s logout URL, so the browser triggers a local logout everywhere. It depends on the browser allowing those requests — third-party cookie restrictions increasingly undermine it.
- Back-channel logout: the provider makes direct server-to-server calls to each app’s registered logout endpoint, carrying a logout token. It doesn’t depend on the browser, which makes it the most reliable — and the modern recommendation where supported.
As with SAML, the honest position is that coordinated logout is best-effort: a single app that’s offline or misimplements its endpoint breaks the chain. Short session lifetimes and revocation at the provider remain the controls you actually rely on. Federating the start of a session is easy; federating its end is a recurring hard problem across every protocol in this module.
Security essentials
OIDC inherits OAuth’s hazards and adds a few of its own around the ID token. The non-negotiables, drawn from OIDC Core and the OAuth security guidance:
- Validate the ID token fully. Verify the signature, and reject
alg: noneoutright — historically a devastating JWT bug where an attacker strips the signature. Then checkiss,aud,exp, and thenonceyou sent. - Use
stateand PKCE.stateblocks CSRF on the redirect; PKCE protects the code exchange — both mandatory in modern OIDC just as in OAuth 2.1. - Bind the
nonce. Generate a fresh nonce per login, send it in the request, and confirm it in the ID token — this is what stops a captured ID token from being replayed. - Guard against IdP mix-up attacks. When a client trusts multiple providers, confirm which issuer actually responded matches the one you started with, so a malicious or confused issuer can’t substitute its token.
- Never identify users by the access token, and never send the ID token to an API as a bearer — the audience-confusion mistakes from the previous section are security bugs, not just style.
- Minimize claims and prefer back-channel logout where supported; keep sessions short regardless.
The pattern rhymes with every protocol in this module: the standard is sound, and almost every real breach is an implementation that validated too little. For the highest-assurance deployments (open banking, government), the FAPI profiles tighten these requirements further.
OIDC and SAML: the same job, different eras
OIDC and SAML both do federated authentication, so when do you reach for which? The contrast at a glance:
| Dimension | SAML 2.0 | OpenID Connect |
|---|---|---|
| Data format | XML | JSON / JWT |
| Transport | Browser redirect + POST | HTTP/REST + redirects |
| Identity artifact | SAML assertion | ID token (+ OAuth access token) |
| Best for | Enterprise & B2B web SSO | Web, mobile, SPA, consumer |
| Origin | 2005, standalone | 2014, built on OAuth 2.0 |
SAML is XML, browser-redirect-based, and entrenched in enterprise and B2B SaaS. OIDC is JSON/JWT, REST-friendly, and the default for anything new — consumer login, mobile apps, SPAs, and increasingly the enterprise too. New build with a choice: prefer OIDC. Integrating with an enterprise app that only speaks SAML: use SAML. Most large organizations run both, and the skill is fluency in each rather than allegiance to one.
Recap
OpenID Connect is authentication built cleanly on OAuth:
- OIDC = OAuth 2.0 + an identity layer. It authenticates the user; OAuth only authorized the client.
- ID token vs access token: the ID token is identity, for the client to read; the access token is authorization, for the API. Don’t swap their jobs.
- The ID token is a JWT of claims —
iss,sub,aud,exp,iat, plus profile/email claims — and it must be validated (signature, iss, aud, exp, nonce) before you trust it. - Discovery, JWKS, and UserInfo make integration self-configuring and key rotation seamless.
- Authorization Code + PKCE is the flow to use; Hybrid is niche; Implicit is deprecated.
- The ID token authenticates a login event, then the client mints its own session and uses the access token for APIs; logout comes in RP-initiated, front-channel, and back-channel forms — back-channel most reliable, none bulletproof, short sessions still essential.
Hands-on exercises
These are practical — do them, don’t just read them:
- Read a provider’s discovery doc. Open
https://accounts.google.com/.well-known/openid-configuration(or any provider’s) in your browser. Identify theauthorization_endpoint,token_endpoint,userinfo_endpoint, andjwks_uri, and note which scopes and signing algorithms are supported. - Decode an ID token. After a “Sign in with…” flow (use a provider’s OIDC playground), paste the ID token into a JWT decoder and label
iss,sub,aud,exp, and two identity claims. Confirmaudequals the client ID. - Tell the tokens apart (use case). Given an ID token and an access token from the same login, decide which you’d send to a backend API and which you’d use to render “Welcome, Sara” — and explain why swapping them is a bug.
- Audit a validation (use case). A client logs users in by base64-decoding the ID token payload and reading
email, skipping signature andaudchecks. List every check it’s missing and the attack each omission enables. - Design logout (use case). You run five apps behind one OIDC provider and need “log out everywhere.” Choose between front-channel and back-channel logout, justify it, and state what you’d still rely on when an app is offline at logout time.
- Map scopes to claims (use case). A new feature needs the user’s name and verified email, nothing more. List the exact scopes you’d request, the claims you’d expect back, and where you’d read each (ID token vs UserInfo) — then explain why requesting
profileplusemailplus a custom scope would be over-asking.
Three questions to test yourself
- Explain, in one or two sentences each, the purpose and intended audience of the ID token versus the access token, and give one concrete bug caused by confusing them.
- Why is
sub— notemail— the correct stable identifier for a user, and what breaks if an app keys its accounts onemail? - A new identity provider needs to be added to your app with zero hard-coded URLs and seamless key rotation. Which OIDC features make that possible, and how do they work together?
Frequently asked questions
What is OpenID Connect (OIDC)?
OpenID Connect is a thin identity layer built on top of OAuth 2.0 that adds real user authentication. Where OAuth only authorizes a client to access resources, OIDC also issues an ID token — a signed JWT that asserts who the user is, who it's for, and when they authenticated. It's the standard behind 'Sign in with Google/Microsoft/Apple,' published by the OpenID Foundation in 2014.
What is the difference between an ID token and an access token?
An ID token is about identity and is meant for the client: a signed JWT containing claims about the user (sub, email, name) that the client validates to log them in. An access token is about authorization and is meant for the resource server: it grants API access and the client should treat it as opaque. Use the ID token to know who the user is; use the access token to call APIs.
Does OpenID Connect replace OAuth 2.0?
No — it's built on OAuth 2.0, not a replacement. OIDC reuses OAuth's flows, endpoints, and tokens and adds the identity pieces OAuth deliberately left out: the ID token, the UserInfo endpoint, standard claims, and discovery. When you 'sign in with' a provider you're using OIDC, which is using OAuth underneath. They're one stack, with OIDC handling authentication and OAuth handling authorization.
What is the .well-known/openid-configuration discovery endpoint?
It's a standard JSON document an OIDC provider publishes at /.well-known/openid-configuration that lists everything a client needs to integrate: the authorization, token, and UserInfo endpoint URLs, the JWKS URI for signature-verification keys, supported scopes, claims, and algorithms. Discovery lets a client configure itself automatically instead of hard-coding URLs, and it's how key rotation stays seamless.
What standard claims does an OIDC ID token contain?
Core claims every ID token carries: iss (issuer), sub (the stable unique user identifier), aud (the client it's for), exp (expiry), and iat (issued-at), often with auth_time and nonce. Profile and email scopes add claims like email, email_verified, given_name, family_name, and name. The client must validate iss, aud, exp, and the signature before trusting any of them.
How does logout work in OpenID Connect?
OIDC defines three mechanisms. RP-initiated logout lets the app send the user to the provider's end-session endpoint to log out centrally. Front-channel logout uses the browser to load hidden logout URLs at each app. Back-channel logout has the provider call each app's logout endpoint server-to-server. As with SAML, coordinated single logout is genuinely hard and often best-effort; short sessions remain the reliable control.