State and nonce
All values in these examples are fictional.
The printer's request carries two values it generated for this attempt: state=demo-signin-3 and nonce=demo-nonce-3. Both are random in a real request, both are stored in the pending sign-in, and both come back. It is reasonable to wonder why one would not be enough.
Two values, two messages
They come back in different messages, and each protects the message it returns in.
state returns in the redirect to the callback, next to the code. As Correlating requests and responses explained, the printer uses it to find the pending attempt and to confirm that the response arrived in the same browser session that started it. A code delivered to a different session is not accepted.
nonce returns inside the ID token. The photo service copies the value from the request into the token's nonce claim and then signs the token. Nobody can change the nonce in a genuine ID token without breaking the signature, and the photo service puts demo-nonce-3 only in a token issued in answer to this request. When the printer finds demo-nonce-3 in a token it has validated, it knows the token belongs to this attempt.
That matters even though the token arrives directly from the token endpoint. Suppose an attacker has stolen the authorization code issued for one of your sign-ins. They start a sign-in of their own at the printer, so their browser has a pending attempt with its own state and nonce, and then deliver your code to the printer's callback in their browser, with their state. The state check passes, because the attempt is theirs. If nothing else stopped it, the printer would exchange your code, receive a perfectly genuine ID token for user-2048, and sign the attacker in to your printer account.
The nonce gives it away. The token carries the nonce from your request, but the attacker's pending attempt expects a different one. The printer rejects the token and signs no one in.
Creating a nonce
A nonce must be unpredictable and used for one attempt only. The printer generates it with the same cryptographically secure randomness it uses for state and the PKCE verifier. The readable demo-nonce-3 is a stand-in.
The printer keeps the expected nonce in the pending sign-in on its server, next to the verifier. A relying party that cannot keep server-side records for each attempt can instead put a random value in a protected cookie and send a hash of it as the nonce, then recompute the hash when the browser returns. Either way, the expected nonce is tied to the browser that started the attempt.
The printer compares the token's nonce claim with the stored value exactly, and only once. When the pending sign-in is claimed, its nonce goes with it, so a second token carrying the same nonce finds nothing to match. If the request sent a nonce and the ID token has none, that is a failure too, not a token that happened to skip an optional check.
A nonce does not stay secret the way the PKCE verifier does. It travels through the browser in the request and appears in the ID token. Its value comes from being unpredictable beforehand and bound to one attempt.
Alongside PKCE
Return to the stolen code. With PKCE, the exchange would never reach the nonce check. The printer would send the verifier stored in the attacker's pending attempt, which does not match the challenge in your request, and the token endpoint would refuse to issue anything.
So the two overlap here. Current OAuth security guidance accepts a properly checked nonce in place of PKCE for confidential OpenID Connect clients such as the printer, as protection against code injection. The printer uses both anyway. They are checked in different places, by different parties, so a mistake in one does not leave the sign-in unprotected.
| Value | Travels in | Checked by | What it establishes |
|---|---|---|---|
state | The request and the redirect | The printer, at the callback | The response belongs to an attempt started in this browser session. |
iss in the response | The redirect | The printer, at the callback | The expected provider answered. |
| PKCE challenge and verifier | The challenge in the request, the verifier in the token request | The photo service, at the token endpoint | Whoever redeems the code started the attempt. |
nonce | The request, then the signed ID token | The printer, when validating the ID token | The ID token was issued for this attempt. |
The nonce carries more weight in older flows that return an ID token directly through the browser, where nothing like the code exchange stands between a captured token and the relying party. OpenID Connect requires a nonce in those flows, and Understanding implicit and hybrid integrations covers them. Expiration and nonce checks shows where the nonce comparison sits among the other checks the printer makes.