OpenID Connect & OAuth 2.0, step by step

Pick a flow and walk through it: who sends what to whom, what is in the request, what comes back and what every single parameter means. The values are not made up – the PKCE challenge is the real SHA-256 of the verifier, and the ID token is signed in your browser. You can read in your own provider through its discovery document.

Choose a flow

The parties

These values are substituted into every request below. Nothing is sent – the provider is simulated, but the values are genuinely computed.

The values of this run

state
nonce
code_verifier
code_challenge

Freshly generated, different every time. The challenge really is the SHA-256 of the verifier – check it or reuse it elsewhere.

Who talks to whom

User / browserApplicationProvider (IdP)Generate valuesRedirect /authorizeSign-in & consentRedirect with codePOST /tokenTokensValidate ID tokenGET /userinfoRefresh
  • Front channel – goes through the browser and is visible
  • Back channel – straight from server to server
  • happens inside one party
1 of 1

The ID token from this run

Not a canned string: this token was just signed in your browser with a freshly generated RSA key. The at_hash genuinely matches the access token above, and the JWK below verifies the signature.

Header

Payload

The claims one by one

FieldValue in this runMeaning
issWho issued the token. Compared character for character during validation.
subThe immutable identifier of the user at the provider. Suitable as a primary key – unlike the email address, which can change.
audWho the token is for. Must contain your own client_id.
expWhen the token stops being valid.
iatWhen it was issued.
auth_timeWhen the user actually authenticated – possibly much earlier, if an existing session was reused.
nonceThe value from the request. Binds this token to exactly this sign-in.
azpThe party the token is intended for. Relevant when aud has several entries.
at_hashDigest of the access token. Proves the two tokens belong together.
sidThe session ID at the provider. Needed for logout notifications.
nameDisplay name. Part of the profile scope.
given_nameFirst name, also from profile.
family_nameLast name, also from profile.
emailEmail address. Only with scope email.
email_verifiedWhether the provider verified the address. Without this being true the address must not be used to match an account.

Matching JWKS entry (jwks_uri)

Read in your own provider

Every OpenID Connect provider describes itself at /.well-known/openid-configuration. Load that document or paste it – the fields get explained, and the endpoints can be carried over into the simulation above.

Note: “Load” is the only point in this tool where your browser calls a foreign address – and only when you click it. The request goes straight from you to the provider, not through our server. Pasted JSON never leaves your browser at all.

OAuth 2.0 or OpenID Connect – which is which?

The two are constantly lumped together, yet they answer different questions.OAuth 2.0 is an authorisation protocol: it governs how an application gets access to someone else's data without ever learning their password – “this app may read my calendar”. It deliberately says nothing about the human behind it.

OpenID Connect is a thin layer on top that adds exactly the missing piece: authentication. Technically it comes down to three ingredients – the openid scope, an extra token in the response (the ID token) and a fixed set of endpoints. Everything else is OAuth. If you understand the OAuth code flow, OIDC takes another five minutes.

The practical consequence shows up in a lot of bug lists: an access token is not a proof of authentication. It attests to a permission, not to an identity, and it is not issued to the application but to the API behind it. Signing users in with an access token builds a vulnerability in by design – the ID token is the piece meant for that job.

Which flow do I need?

ApplicationFlowWhy
Server-side applicationAuthorization code + PKCECan genuinely keep a client_secret. Use PKCE anyway – it costs nothing and protects against intercepted codes.
Browser SPAAuthorization code + PKCEA public client, so no secret. PKCE replaces it entirely.
Mobile or desktop appAuthorization code + PKCEIn the system browser, not an embedded WebView – otherwise the app can see the password after all.
TV, console, CLIDevice authorization grantMoves the typing to a device that has a keyboard and a browser.
Background service, cron jobClient credentialsNo human involved, so no authentication and no ID token.
Legacy system using response_type=tokenImplicit – migrateConsidered obsolete and absent from OAuth 2.1. The replacement is the same code flow as everywhere else.

state, nonce and PKCE: three random values, three different jobs

They look alike – all three are random strings that travel out and back – and are therefore routinely confused or dismissed as redundant. In fact they secure three completely different things, and none of them substitutes for another.

ValueKept byPrevents
statethe client sessionSomeone slipping the user a foreign callback and thereby signing them into an attacker's account (CSRF).
noncethe client session, later the ID tokenA valid ID token captured elsewhere being replayed a second time.
code_verifierthe client aloneAn intercepted authorization code being redeemed by somebody else.

The difference is visible in the route each one takes: state and noncetravel openly through the browser and are compared on the way back. The code_verifiernever goes through the browser at all – only its digest does. That makes PKCE the only one of the three that still holds up when an attacker reads the entire front channel.

ID token, access token, refresh token

TokenRecipientAnswersLifetime
ID tokenthe clientWho signed in, when and how?Minutes – it is validated once, not kept
Access tokenthe APIWhat is the caller allowed to do?Minutes to hours
Refresh tokenthe providerMay I have new tokens?Days to months

Two frequently broken rules follow from this. First: an ID token never belongs in anAuthorization header to an API. It is issued to the client (aud is theclient_id), and an API that accepts it anyway is checking the wrong audience. Second: a client should not parse the access token. It may well be a readable JWT, but it does not have to be – the format belongs to the provider and the API and can change at any time.

Scopes and claims

A scope is what you ask for; a claim is what comes back. OpenID Connect defines fixed bundles: profile brings name, picture and locale,email the address along with email_verified, with addressand phone to match. offline_access is the odd one out – it does not ask for data but for a refresh token.

email_verified deserves a second look: if the value is not true, the address must not be used to match an existing account. Otherwise it is enough to create an account with someone else's address at a provider that never verifies it, and walk straight into their account. The only stable identifier is the combination of iss and sub.

Discovery: the provider describing itself

At /.well-known/openid-configuration every OpenID Connect provider describes itself: endpoints, supported scopes, signature algorithms, PKCE methods. For an integration this is the most reliable source there is – more reliable than the documentation, because it comes out of the running system. The tool above reads it in, explains every field and answers the usual questions straight away: is S256 supported? Is there a device flow? Are unsigned ID tokens on offer (which would be a red flag)?

One detail decides the security of the whole chain: the issuer in the document must match the address the document is served from. Later on, the iss claim of every ID token is checked against exactly this value – character for character, not “starts with”.

Signing out is harder than signing in

Local logout is trivial: drop the client's session and you are done. Except the user is still signed in at the provider – the next sign-in attempt sails through without a prompt and looks like a bug. That is what RP-initiated logout via the end_session_endpoint is for, with id_token_hint and post_logout_redirect_uri.

The other direction is nastier still: when someone signs out at the provider, every connected application should learn about it. The front-channel approach (hidden iframes) depends on third-party cookies and is disappearing along with them; only back-channel logout, where the provider notifies each application server-side, is dependable. That requires the client to expose a reachable endpoint – something a pure SPA with no backend cannot provide.

Frequently asked questions

Is anything sent to a server here?
No. The provider in the tool is simulated and every value is produced in your browser. The one exception is the “Load” button in the discovery section: it calls the address you type there – directly from your browser to that provider, not through our server. Pasted JSON never leaves your browser at all.

Can I run this against my real provider?
Not as a full sign-in – that would need a registered redirect_uri and would mean accepting someone else's tokens inside a third-party page. What you can do: read in the discovery document, adopt the endpoints and compare the resulting requests with the ones your own application sends. In practice that is exactly where the difference turns up – a missing nonce, aredirect_uri that differs by one trailing slash.

Is the ID token above real?
Yes. It is signed when the page loads, using a freshly generated RSA key via the Web Crypto API; the JWK set shown alongside belongs to it and verifies the signature successfully. Theat_hash is genuinely computed over the access token next to it. To check for yourself:JWT decoder.

Why does client credentials return no ID token?
Because there is nobody to attest to. An ID token is the statement “this person signed in here and now”. A background service involves no person, only an application identifying itself. That is why this flow is plain OAuth 2.0 and not OpenID Connect at all.

Do I need PKCE even with a client_secret?
Yes, and the IETF recommendation is unambiguous here. The secret protects the token endpoint; PKCE protects the authorization code on its way through the browser – two different routes. The cost is two extra parameters that every library sets for you anyway.