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
Front channel – goes through the browser and is visible
Back channel – straight from server to server
happens inside one party
1 of 1
localApplication
The client rolls its one-time values
Before anything goes over the wire, the application generates three random values. The code_verifier stays secret inside the client; only its SHA-256 digest (the code_challenge) travels along. That makes an intercepted authorization code worthless to an attacker: without the verifier it cannot be redeemed. The values here are genuinely computed – the challenge really is the Base64url-encoded SHA-256 of the verifier above it.
Field
Value in this run
Meaning
staterecommended
Random value against CSRF. The client stores it in its session and compares it on the way back. A response with a foreign or missing state is discarded.
noncerecommended
Random value against token replay. It ends up as a claim inside the ID token; the client checks that the same value comes back.
code_verifierrequired
43 to 128 characters from the URL-safe alphabet. It only leaves the client in the token request – over the back channel, never through the browser.
code_challengerequired
BASE64URL(SHA-256(code_verifier)). Travels openly through the browser but cannot be reversed.
code_challenge_methodrequired
S256
Always S256. The value plain sends the verifier in the clear and exists only for devices that cannot do SHA-256 – so, in practice, never.
Front channelApplication → Provider (IdP)GET
Redirect to the authorization server
The application sends the user's browser to the authorization endpoint – an ordinary redirect. Everything here is visible to the user and sits in the browser history: this leg is the front channel. Secrets have no business here, which is why only the challenge travels and not the verifier.
code says: give me an authorization code, not tokens in the browser.
client_idrequired
The application's public identifier at the provider. Not a secret.
redirect_urirequired
Where the provider sends the browser back to. Must be registered.
scoperequired
What is being requested. openid is what turns OAuth 2.0 into OpenID Connect – without it there is no ID token.
staterecommended
The CSRF value from step 1, returned unchanged.
noncerecommended
The value from step 1, reappearing inside the ID token.
code_challengerequired
The SHA-256 digest of the verifier.
code_challenge_methodrequired
S256
S256 – see the previous step.
promptoptional
login | consent | none | select_account
Controls the login screen: login forces a fresh authentication, consent re-asks for approval, none forbids any interaction (for silent renewal in the background) and otherwise returns the error login_required.
max_ageoptional
3600
How old the authentication may be, in seconds. If the session is older, the user must authenticate again; the result appears as auth_time in the ID token.
login_hintoptional
erika@example.com
Pre-fills the username, saving the user some typing.
ui_localesoptional
de-DE
Preferred languages for the login screen, ordered by preference.
The redirect_uri must be pre-registered with the provider and match exactly – character for character, trailing slash included. That is not bureaucracy: it is the only thing stopping someone from diverting the code to their own server.
Front channelUser / browser → Provider (IdP)
The user signs in – with the provider, not with the application
This is the part the application never sees and never should: password, second factor, passkey, consent dialog. The client sees none of it – that is the actual payoff of the whole protocol. If a session already exists at the provider, this step passes invisibly, and that is precisely what single sign-on is.
Because the provider keeps its own session in a cookie, silent renewal in the background (prompt=none inside a hidden iframe) depends on third-party cookies – which browsers are busy switching off. The intended replacement is refresh tokens with rotation.
Front channelProvider (IdP) → Application302
Back again, with the authorization code
The provider sends the browser back to the registered address and appends the code. The code is short-lived (usually a minute) and redeemable exactly once; on its own it grants access to nothing. It sits in the address bar, in the history and possibly in a server log – which is exactly why it is only half the story and needs the verifier in the next step.
HTTP/1.1 302 Found
Location: ?code=
&state=
&iss=
Field
Value in this run
Meaning
coderequired
The authorization code. Single use, short lifetime.
staterecommended
Must match the value from step 1 byte for byte.
issoptional
The issuer, added by RFC 9207. It protects applications with several providers from redeeming a code at the wrong server – the mix-up attack.
This is where state gets checked, before anything else happens. If it is missing or does not match the session, the client aborts. The comparison should be constant-time, and the stored value deleted afterwards – otherwise it can be reused.
Back channelApplication → Provider (IdP)POST
Trading the code for tokens
This call goes straight from the client to the provider, with no browser in between: the back channel. Here the code_verifier is handed over. The provider computes the SHA-256 itself and compares it against the challenge from step 2. If they differ there are no tokens – even if the code is perfectly valid.
POST HTTP/1.1
Host:
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=
&redirect_uri=
&client_id=
&code_verifier=
Field
Value in this run
Meaning
grant_typerequired
authorization_code
Tells the token endpoint which kind of exchange this is.
coderequired
The code from the redirect.
redirect_urirequired
The same value as in step 2, again. The provider compares it – that binds the code to the original request.
client_idrequired
For public clients, the only statement of identity.
code_verifierrequired
The counterpart to the challenge. Only now does it leave the client – and only here.
client_secretconditional
Confidential clients only. HTTP Basic auth is more common than the body; better still is private_key_jwt, where no secret travels at all.
A client_secret belongs only in clients that can actually keep it: server-side applications. In an SPA or a mobile app it stops being a secret the moment someone opens the network tab or unpacks the bundle. That is what PKCE is for – a public client without a secret is secure regardless.
Back channelProvider (IdP) → Application200
The response: three tokens with three jobs
The provider answers with a JSON object. The three tokens inside are endlessly confused with one another, yet they have completely different recipients: the ID token is for the client and says who signed in. The access token is for the API and says what is allowed – the client is not supposed to read it at all. The refresh token is for the provider and exists only to obtain new tokens.
The key to the API. May be a JWT or an opaque string – either way, to the client it is a value to pass on, not to inspect.
token_typerequired
Bearer
Practically always Bearer: whoever holds it may use it. Handle accordingly.
expires_inrecommended
3600
Remaining lifetime of the access token in seconds, counted from this response.
id_tokenconditional
The result of the authentication, as a signed JWT. Only with scope=openid.
refresh_tokenoptional
Obtains new tokens without asking the user again. Far longer-lived, and therefore the most sensitive of the three.
scopeconditional
What was actually granted – possibly less than was asked for.
The ID token below is a real RS256 token signed at runtime inside your browser, not a canned string – including a correctly computed at_hash over the access token.
localApplication
Validate the ID token (this part is mandatory)
An ID token you do not validate is merely a claim. Validation is the point where a chunk of Base64 turns into a dependable statement about an identity – and it is where hand-rolled integrations most often go wrong. Libraries handle it for you; anyone building it themselves has to tick off every item on this list.
Field
Value in this run
Meaning
issrequired
Must be exactly the expected issuer – string comparison, not a prefix match.
audrequired
Must contain your own client_id. A token issued for another application is not a valid credential.
exprequired
Must lie in the future. A little clock skew tolerance is customary, more than a few minutes is not.
noncerequired
Must match the value from step 1.
azpconditional
With multiple audiences: the application the token was meant for.
at_hashconditional
The left half of the SHA-256 over the access token, Base64url-encoded. It binds the two tokens together.
Always pin the expected algorithm yourself, never take it from the token header. Otherwise an attacker sends alg: none, or swaps RS256 for HS256 and signs with the public key they already have.
Back channelApplication → Provider (IdP)GET
Querying UserInfo – often unnecessary
The UserInfo endpoint returns the same claims once more, this time against the access token. It earns its keep when the data must be current or the ID token should stay small – some providers only serve standard claims here at all. If the ID token already carries everything you need, this call is an extra round trip for nothing.
The sub in the response must match the sub from the ID token. If it does not, two different people have been conflated and the response must be discarded.
Back channelApplication → Provider (IdP)POST
Getting more, without bothering the user
When the access token expires, the client trades the refresh token for a fresh pair. Nothing visible happens for the user. The requested scope may shrink in the process, but it may never grow beyond the original grant.
POST HTTP/1.1
Host:
Content-Type: application/x-www-form-urlencoded
grant_type=refresh_token
&refresh_token=
&client_id=
&scope=
Field
Value in this run
Meaning
grant_typerequired
refresh_token
Exchanging a refresh token for new tokens.
refresh_tokenrequired
The token from the last response – a different one each time when rotation is on.
client_idconditional
Required for public clients; confidential ones authenticate instead.
scopeoptional
Optionally narrow it. Widening is not possible.
Public clients must rotate refresh tokens: every use returns a new one and invalidates the old. If a spent token shows up again it was stolen, and the provider revokes the entire chain. This is exactly why a refresh token never belongs in localStorage in a browser.
Back channelApplication → Provider (IdP)POST
The device asks for a code
A TV has no usable keyboard and often no browser at all. Instead of making the user type there, the device fetches two codes and moves the sign-in to a device that is good at it – the phone in their pocket.
POST HTTP/1.1
Host:
Content-Type: application/x-www-form-urlencoded
client_id=
&scope=
Field
Value in this run
Meaning
client_idrequired
The identifier of the device or the application on it.
scopeoptional
With openid there is an ID token at the end here too.
Back channelProvider (IdP) → Application200
Two codes: one for the human, one for the machine
The user_code is short and deliberately easy to type – usually without the characters people mix up. It goes on the screen. The device_code is long, is never displayed, and is the value the device is about to poll with.
The device's secret. For polling only, never display it.
user_coderequired
The short code to type. Shown on screen.
verification_urirequired
The address the user opens on their phone.
verification_uri_completeoptional
The same address with the code built in – for the QR code.
expires_inrequired
900
How long the code pair is valid, typically 10 to 15 minutes.
intervaloptional
5
Minimum delay between two polling attempts, in seconds.
verification_uri_complete already contains the user_code – ideal as a QR code on the TV. The user scans it and types nothing at all.
Front channelUser / browser → Provider (IdP)
The user completes the sign-in on their phone
On the phone the ordinary authentication flow runs, second factor included – with a real keyboard, a familiar browser and a visible address bar. The device in the living room sees none of it and never gets to touch a password.
The weak spot of this flow is phishing: an attacker starts it themselves and gets the victim to approve their code. The consent page must therefore state clearly which device is being granted access – and the flow belongs only on devices that genuinely need it.
Back channelApplication → Provider (IdP)POST
The device asks again, at a steady pace
Meanwhile the device asks the token endpoint at regular intervals whether the user is done. Until then the answer is an error – and here that error is not a malfunction but the expected state.
POST HTTP/1.1
Host:
Content-Type: application/x-www-form-urlencoded
grant_type=urn:ietf:params:oauth:grant-type:device_code
&device_code=
&client_id=
HTTP/1.1 400 Bad Request
{ "error": "authorization_pending" }
Field
Meaning
authorization_pendingrequired
All fine, the user is not done yet. Keep waiting.
slow_downrequired
Asking too fast. Increase the interval before trying again.
access_deniedrequired
The user declined. Final – do not retry.
expired_tokenrequired
The code pair has expired. The flow starts over.
slow_down is not a suggestion: the interval must then grow by at least five seconds, or the provider will shut the attempt down.
Back channelProvider (IdP) → Application200
Consent granted – tokens for the device
Once the user approves on their phone, the next polling attempt answers itself: the device receives an access token, a refresh token and – with scope=openid – an ID token. From here on it is indistinguishable from the code flow.
Because devices can rarely be signed in again, refresh tokens here are unusually long-lived. They sit on a box in someone's living room – being able to revoke them centrally is a requirement, not a nicety.
Back channelApplication → Provider (IdP)POST
A service identifies itself
No browser, no user, no redirect: a background service needs access to an API and authenticates with its own credentials. The entire flow is a single HTTP call. The client_id is the identity here; there is no person behind it.
POST HTTP/1.1
Host:
Authorization: Basic
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&scope=invoices.read
Field
Value in this run
Meaning
grant_typerequired
client_credentials
The service is acting on its own behalf.
scopeoptional
invoices.read
Which API permissions are needed. No openid here – more on that below.
client_idconditional
The service's identity.
client_secretconditional
The matching secret, encoded into Basic auth here.
The credentials belong in Basic auth or – better – in a signed client assertion JWT (private_key_jwt). Then the secret never leaves the service, and an intercepted call contains nothing reusable.
Back channelProvider (IdP) → Application200
One access token – and nothing else
The response contains only an access token. No ID token, no refresh token, and that is not an oversight by the provider but correct: an ID token attests that a human signed in. Nobody signed in here. A refresh token would be equally pointless – the service knows its secret and can fetch a new token whenever it likes.
This is why this flow is not OpenID Connect but plain OAuth 2.0. Sending scope=openid here gets you either an error or, depending on the provider, an ID token that asserts nothing meaningful.
Front channelApplication → Provider (IdP)GET
Requesting tokens directly – the old way
Before PKCE, a browser-only application could not safely talk to the token endpoint: a client_secret would have sat in the source, and CORS was not available everywhere. The answer at the time was to deliver the tokens with the redirect itself – no second step.
id_token token delivers both tokens at once, id_token alone just the authentication.
noncerequired
Mandatory in the implicit flow – it is the only thing binding the token to this request.
response_modeoptional
fragment | form_post
fragment is the default. form_post delivers the tokens by POST instead, keeping them out of the address bar.
The middle ground, response_type=code id_token, is called the hybrid flow: the ID token arrives immediately while the code is exchanged as usual. You still meet it in first-generation OpenID Connect implementations and in Microsoft Entra configurations.
Front channelProvider (IdP) → Application302
The tokens are sitting in the address bar
The part after the # is not sent to the server, but it is anything but protected: it lands in the browser history, is readable by every script on the page, ends up in extensions and gets copied along when someone shares the link. An access token that has passed through an address bar has to be treated as compromised.
HTTP/1.1 302 Found
Location: #access_token=
&token_type=Bearer
&expires_in=3600
&id_token=
&state=
There is no refresh token here – when the access token expired, the only remedy was a hidden iframe with prompt=none. Today that breaks on the browsers' third-party cookie rules, so it is finished in practice as well.
localApplication
Why it is discouraged – and what applies instead
The current IETF recommendation (OAuth 2.0 Security Best Current Practice) is unambiguous: the implicit flow should no longer be used, and OAuth 2.1 simply omits it. Every kind of browser application today uses authorization code with PKCE – the same mechanism as server-side applications, just without a client_secret.
If you run into an existing integration using response_type=token: that is a reason to migrate, not merely to modernise. The path is usually short, because every current library ships code+PKCE as its default anyway.
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.
This flow returns no ID token – nobody signs in, so there is nothing to attest to.
Who issued the token. Compared character for character during validation.
sub
The immutable identifier of the user at the provider. Suitable as a primary key – unlike the email address, which can change.
aud
Who the token is for. Must contain your own client_id.
exp
When the token stops being valid.
iat
When it was issued.
auth_time
When the user actually authenticated – possibly much earlier, if an existing session was reused.
nonce
The value from the request. Binds this token to exactly this sign-in.
azp
The party the token is intended for. Relevant when aud has several entries.
at_hash
Digest of the access token. Proves the two tokens belong together.
sid
The session ID at the provider. Needed for logout notifications.
name
Display name. Part of the profile scope.
given_name
First name, also from profile.
family_name
Last name, also from profile.
email
Email address. Only with scope email.
email_verified
Whether 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.
What follows from it
The fields one by one
Field
Value in this run
Meaning
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?
Application
Flow
Why
Server-side application
Authorization code + PKCE
Can genuinely keep a client_secret. Use PKCE anyway – it costs nothing and protects against intercepted codes.
Browser SPA
Authorization code + PKCE
A public client, so no secret. PKCE replaces it entirely.
Mobile or desktop app
Authorization code + PKCE
In the system browser, not an embedded WebView – otherwise the app can see the password after all.
TV, console, CLI
Device authorization grant
Moves the typing to a device that has a keyboard and a browser.
Background service, cron job
Client credentials
No human involved, so no authentication and no ID token.
Legacy system using response_type=token
Implicit – migrate
Considered 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.
Value
Kept by
Prevents
state
the client session
Someone slipping the user a foreign callback and thereby signing them into an attacker's account (CSRF).
nonce
the client session, later the ID token
A valid ID token captured elsewhere being replayed a second time.
code_verifier
the client alone
An 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
Token
Recipient
Answers
Lifetime
ID token
the client
Who signed in, when and how?
Minutes – it is validated once, not kept
Access token
the API
What is the caller allowed to do?
Minutes to hours
Refresh token
the provider
May 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.