OpenID Connect: the identity layer on top of OAuth
How OIDC turns OAuth 2.0 into real authentication: the ID token (a JWT) versus the access token, standard claims (sub, iss, aud, exp, email), the discovery and UserInfo endpoints, JWKS validation, the Authorization Code, Hybrid, and Implicit flows, and RP-initiated, front-channel, and back-channel logout.
“Sign in with Google”
Daniel is trying a new project management tool. Instead of creating another password, he clicks “Sign in with Google,” picks his account, and lands on his dashboard with his name and photo already filled in. Under that button, the tool used OAuth exactly as the previous article described: a redirect, a consent screen, an authorization code, a token exchange. But it also got something OAuth on its own doesn’t provide, a trustworthy statement of who Daniel is.
That statement is what OpenID Connect (OIDC) adds. The OAuth article ended on a warning: OAuth authorizes a client but doesn’t authenticate the user, and apps that used an access token as a sign-in created real security holes. OIDC is the fix the industry agreed on, a thin identity layer 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.
What makes it work is how little it adds. It reuses OAuth’s flows, endpoints, and access tokens unchanged and adds exactly what OAuth left out on purpose: a token that says 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 all. OAuth asks whether this app may do this; OIDC adds the question of who, exactly, this user is.
The standardization is the point. Before OIDC, every provider offering social login did it with its own slightly different OAuth extension, and developers wrote custom code for each one. OIDC replaced that with one ID token format, one discovery document, and one validation procedure that behaves the same against Google, Microsoft, Okta, Auth0, or any compliant provider. Learn it once and integrate anywhere, which is why it spread so quickly.
ID token and access token
The heart of OIDC is a second token next to OAuth’s access token, with a completely different purpose and audience. Getting the 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 meant for the client, and is a JWT the client reads and checks to authenticate the user. The access token is about authorization, is meant for the resource server, and the client treats it as opaque and only forwards it to the API. The client uses the ID token to sign 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 and checks it<br/>to sign the user in] AT --> RS[Client forwards it<br/>to the resource server]
The ID token is about identity and is meant for the client. It’s a signed JWT the client opens, checks, and reads to establish who just signed in, and its audience (aud) is the client itself. The access token is about authorization and is meant for the resource server. The client should treat it as opaque: it doesn’t read it, it just forwards it to APIs, and its audience is the resource server.
The classic mistakes are using the access token to identify the user, which it isn’t meant for and which may not even be a JWT you can read, and sending the ID token to an API as if it were an access token, when the API isn’t its audience. The ID token tells you who; the access token is how you call what.
The ID token: a JWT full of claims
The ID token is a JWT, a JSON Web Token: three base64url parts, header, payload, and signature, that the client can decode and verify. Its payload is a set of claims about the sign-in and the user. Decoded, it 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": "López"
}The claims fall into two groups. The protocol claims make the token trustworthy. iss is the issuer, the provider that created the token, and the client must recognize it. sub is the subject, a stable, unique identifier for the user at this issuer; it, and not the email, is the real primary key, because emails change and sub doesn’t. aud is the audience, the client ID the token was issued for, and the client must confirm it’s the intended audience. exp and iat are the expiry and issue times that bound the token’s validity. And auth_time and nonce record when the user actually authenticated and carry a value the client generated to tie the token to its own request and block replay.
The identity claims describe the user: email, email_verified, given_name, family_name, name, picture, and so on, filled in according to the scopes requested. OIDC ties claims to scopes, so the client gets 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, and others |
email | email, email_verified |
address | address |
phone | phone_number, phone_number_verified |
Always ask for openid, which is mandatory and is what triggers the ID token, and add profile or email only if you really need them. Asking for few claims is the identity-layer version of OAuth’s narrow scopes: don’t request attributes you won’t use.
An ID token means nothing until it’s been checked. Verify the signature against the provider’s public key, confirm iss is the expected issuer, confirm aud is your client ID, confirm exp hasn’t passed, and confirm the nonce matches the one you sent. Reading email out of an unchecked JWT and signing the user in is the OIDC version of trusting an unsigned note, and it’s a real, recurring vulnerability.
Discovery, JWKS, and UserInfo
OIDC standardizes how a client learns about a provider and gets the keys it needs to check tokens, which is what turns “add a new identity provider” into configuration rather than a coding project.
Every compliant provider publishes a discovery document at /.well-known/openid-configuration listing its authorization_endpoint, token_endpoint, userinfo_endpoint, its jwks_uri, and the scopes, claims, and algorithms it supports. A client points at the issuer’s URL and configures itself. The jwks_uri serves the provider’s public signing keys as a JSON Web Key Set. The client fetches them to verify ID token signatures, and because keys are selected by kid, a key ID, the provider can rotate its signing keys without breaking anyone. And UserInfo is an OAuth-protected endpoint the client calls with the access token to get more claims about the user, for when it needs 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. It then fetches the JSON Web Key Set from the JWKS URI to get the provider's public signing keys. When the client later receives an ID token, it picks 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) C->>C: Later, verify the ID token (kid, iss, aud, exp, nonce)
The flows
OIDC uses OAuth’s grant types, adapted for authentication by adding the openid scope, which tells the provider to issue an ID token as well.
Authorization Code, with PKCE, is the standard flow for essentially everything: web apps, single-page apps, and mobile. The client gets a code through a redirect and exchanges it for an ID token and an access token. This is the one to use. Hybrid returns some tokens directly from the authorization endpoint and others from the token endpoint. It exists for particular scenarios but adds complexity and exposes tokens on the front channel, and most teams don’t need it. Implicit returned tokens directly in the redirect, like OAuth’s implicit grant, and is deprecated for the same reasons: tokens leak into browser history and logs. Authorization Code with PKCE replaces it everywhere, including single-page apps.
Authorization Code in OIDC
The flow is OAuth’s Authorization Code with PKCE plus one addition, the openid scope, which makes the provider return an ID token.
sequenceDiagram accTitle: OpenID Connect Authorization Code flow accDescr: The client sends the user to the OIDC provider's authorization endpoint asking for the openid scope, with 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 the PKCE verifier at the token endpoint and receives an ID token and an access token. The client checks the ID token to authenticate the user, and can then call 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 and consent P->>C: Authorization code (one-time) C->>P: Exchange code + code_verifier P->>C: ID token + access token C->>C: Check ID token (sig, iss, aud, exp, nonce) C->>P: (optional) UserInfo with access token P->>C: More claims
The only conceptual difference from plain OAuth is the ID token coming back and the client checking it to authenticate the user. Everything else, the redirects, PKCE, the code exchange, is OAuth you already know. That reuse is why OIDC won: it gave developers authentication almost for free on top of an authorization framework they were already deploying.
From ID token to session
Something that trips up newcomers: the ID token is proof that a sign-in happened, at a particular moment. It isn’t a long-lived session token, and it isn’t meant to be sent to your own APIs on every request. Its job is to be checked once, right after sign-in, so the client knows who the user is.
What comes next is up to the client. Having checked the ID token, the app creates its own session, usually a secure, HTTP-only session cookie, and that session, not the ID token, carries the user through later requests. The ID token has done its job and can be thrown away.
After that, the two tokens go separate ways. If the app needs to keep calling APIs as the user, that’s authorization: it uses the access token and renews it with the OAuth refresh token when it expires, and the ID token plays no part. If the app needs to keep the user signed in, that’s its own session’s job, governed by its own timeout policy and independent of the tokens.
A common bug is sending the ID token to a back end as a bearer credential, mixing up “who signed in” with “may call this API.” Keep them apart: the ID token authenticates the sign-in, the access token authorizes calls, and your session cookie provides continuity. The separation also explains why logout is its own problem. Ending your own session is easy; ending the provider’s session and every other app’s is the hard part.
Logout: three mechanisms, one hard problem
Signing in is the easy half. Signing out cleanly across many apps that share a 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 send the user to the provider's end-session endpoint to end the central session. Front-channel logout has the browser load hidden logout URLs at each relying party. Back-channel logout has the provider call each relying party's logout endpoint directly, server to server. Back-channel is the most reliable because it doesn't 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's server] FC --> R[Sessions ended<br/>best effort] BC --> R
With RP-initiated logout, the relying party, the app, sends the user to the provider’s end_session_endpoint, which ends the central session at the provider. It’s the baseline and the most widely supported. With 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, and restrictions on third-party cookies increasingly undermine it. With back-channel logout, the provider calls each app’s registered logout endpoint directly, server to server, carrying a logout token. It doesn’t depend on the browser, which makes it the most reliable and the current recommendation where it’s supported.
As with SAML, the honest position is that coordinated logout is best effort: one app that’s offline, or that implements its endpoint badly, breaks the chain. Short session lifetimes and revocation at the provider are 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 around the ID token. The non-negotiables come from OIDC Core and the OAuth security guidance.
Check the ID token fully. Verify the signature and reject alg: none outright, historically a devastating JWT bug in which an attacker simply strips the signature. Then check iss, aud, exp, and the nonce you sent. Use state and PKCE: state blocks cross-site request forgery on the redirect and PKCE protects the code exchange, both mandatory in modern OIDC as in OAuth 2.1. Bind the nonce: generate a fresh one per sign-in, send it, and confirm it in the ID token, which is what stops a captured ID token from being replayed.
If a client trusts several providers, guard against mix-up attacks by confirming that the issuer that actually responded is the one you started with, so a malicious or confused issuer can’t swap in its own token. Never identify users by the access token, and never send the ID token to an API as a bearer token; the audience confusion described earlier is a security bug, not a matter of style. And ask for few claims, prefer back-channel logout where it’s supported, and keep sessions short regardless.
The pattern rhymes with every protocol in this module: the standard is sound, and nearly every real breach is an implementation that checked too little. For the highest-assurance uses, open banking and government, the FAPI profiles tighten these requirements further.
OIDC and SAML: the same job, different eras
OIDC and SAML both handle federated authentication, so when do you use which?
| SAML 2.0 | OpenID Connect | |
|---|---|---|
| Data format | XML | JSON and JWT |
| Transport | Browser redirects and POST | HTTP, REST, and redirects |
| Identity artifact | SAML assertion | ID token (with an OAuth access token) |
| Best for | Enterprise and B2B web single sign-on | Web, mobile, single-page apps, consumers |
| Origin | 2005, standalone | 2014, built on OAuth 2.0 |
SAML is XML, based on browser redirects, and entrenched in enterprise and B2B SaaS. OIDC is JSON and JWT, friendly to REST, and the default for anything new: consumer sign-in, mobile apps, single-page apps, and increasingly the enterprise too. If you’re building authentication today and the other side supports it, choose OIDC with Authorization Code and PKCE; it works for web, mobile, and single-page apps alike and reuses everything you learned about OAuth. Fall back to SAML when you have to integrate with an estate that only speaks SAML. Most large organizations run both, and the skill is being fluent in each rather than loyal to one.
Recap
OpenID Connect is authentication built cleanly on OAuth.
- OIDC is OAuth 2.0 plus an identity layer. It authenticates the user; OAuth only authorized the client.
- The ID token is about identity, for the client to read; the access token is about authorization, for the API. Don’t swap their jobs.
- The ID token is a JWT of claims,
iss,sub,aud,exp,iat, plus profile and email claims, and it must be checked (signature, issuer, audience, expiry, nonce) before you trust it. Usesub, not email, as the user’s key. - Discovery, JWKS, and UserInfo let integrations configure themselves and make key rotation seamless.
- Authorization Code with PKCE is the flow to use; Hybrid is niche; Implicit is deprecated.
- The ID token proves a sign-in happened; the client then creates its own session and uses the access token for APIs. Logout comes in RP-initiated, front-channel, and back-channel forms, back-channel being the most reliable, none bulletproof, and short sessions still essential.
Hands-on exercises
These are meant to be done, not just read.
- Read a provider’s discovery document. Open
https://accounts.google.com/.well-known/openid-configuration, or any provider’s, in your browser. Find 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, using 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. Given an ID token and an access token from the same sign-in, decide which you’d send to a back-end API and which you’d use to show “Welcome, Sara,” and explain why swapping them is a bug.
- Audit a validation. A client signs users in by base64-decoding the ID token’s payload and reading
email, skipping the signature andaudchecks. List every missing check and the attack each omission allows. - Design logout. You run five apps behind one OIDC provider and need “log out everywhere.” Choose between front-channel and back-channel logout, justify it, and say what you’d still rely on when an app is offline at logout time.
- Map scopes to claims. 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, and where you’d read each (ID token or UserInfo), then explain why requesting
profile,email, and a custom scope would be asking for too much.
Three questions to test yourself
- In one or two sentences each, explain the purpose and intended audience of the ID token and the access token, and give one concrete bug caused by confusing them.
- Why is
sub, and notemail, the right stable identifier for a user, and what breaks if an app keys its accounts on email? - You need to add a new identity provider to your app with no 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 on top of OAuth 2.0 that adds real user authentication. OAuth only authorizes a client to access resources; OIDC also issues an ID token, a signed JWT that states who the user is, which app it's for, and when they authenticated. It's the standard behind 'Sign in with Google, Microsoft, or 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 with claims about the user (sub, email, name) that the client checks to sign 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 and the access token to call APIs.
Does OpenID Connect replace OAuth 2.0?
No, it's built on OAuth 2.0. 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 uses 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 listing everything a client needs to integrate: the authorization, token, and UserInfo endpoints, the JWKS URI for verification keys, and the supported scopes, claims, and algorithms. It lets a client configure itself instead of hard-coding URLs, and it's how key rotation stays seamless.
What standard claims does an OIDC ID token contain?
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. The profile and email scopes add claims such as email, email_verified, given_name, family_name, and name. The client must check 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 sends the user to the provider's end-session endpoint to log out centrally. Front-channel logout has the browser 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 logout is hard and often best effort, so short sessions remain the reliable control.