Correlating requests and responses
The printer's callback endpoint is an address on the web. Someone can send a request to it without first selecting Connect. You could also have two tabs open, each connecting a different photo account.
The client needs more context than "a code arrived." It must know which attempt the response belongs to, which browser session started it, and which authorization server it expected to answer.
Keeping a pending transaction
When the printer starts the connection, it creates a record similar to this simplified example:
Pending attempt: demo-attempt-7
Initiating session: the current printer session
Expected issuer: https://auth.photos.example
Redirect URI: https://printer.example/oauth/callback
PKCE verifier: held privately on the backend
Return destination: a validated local photo-book page
Status: pending, with a limited lifetime
The actual state value should be fresh and unpredictable. The readable value here only helps us follow the example.
On return, the printer uses state to locate the pending attempt and verifies its binding to the current session. A matching value in a global list is not enough if the response can be attached to a different person's session.
That session binding is what stops cross-site request forgery, or CSRF. In a CSRF attack, another site causes your browser to send a request you did not intend, and the receiving application treats it as yours. Against the printer, an attacker starts a connection with their own photo account and stops before their browser returns to the printer. They then get your browser to open the callback address with their code, perhaps through a link, or a page you visit that sends your browser there. Without the check, the printer would attach the attacker's photo account to your printer session, and you would be choosing photos for your book from an account the attacker controls. With it, the response carries a state value that belongs to no pending attempt in your session, and the printer stops.
The attempt must still be valid and available for processing. The application should coordinate processing so two callbacks cannot both complete the same pending attempt. Separate records for separate attempts avoid overwriting one tab's verifier with another tab's value.
If the original session has expired, the printer should not guess which local account should receive the connection. It can explain that the attempt expired and let the person start again from an appropriate session.
Checking who answered
An application that connects to more than one authorization server must also know which one answered. Suppose the printer also imports photos from a second service, Pixel Vault, using the same callback address for both, and that Pixel Vault is malicious or has been compromised.
You choose Pixel Vault, so the printer records https://auth.pixelvault.example as the expected issuer and sends your browser there. Pixel Vault immediately forwards your browser to the genuine photo service, with a request that uses the printer's photo service client ID. That ID is public, so Pixel Vault knows it. You see the familiar photo service and approve. The code returns to the printer's callback, and the printer, still expecting Pixel Vault, sends it to Pixel Vault's token endpoint along with its PKCE verifier. Pixel Vault now holds a code for your genuine photo account. Nothing was wrong with the code itself. The printer sent it to the wrong server. This is called authorization server mix-up.
Our photo service supports the iss response parameter, which catches this. The response names https://auth.photos.example, but the issuer saved for this attempt is Pixel Vault. The printer compares the two exactly and stops before sending the code anywhere. It also stops if an expected iss parameter is missing, and it never uses a returned issuer value to select a new token endpoint.
Issuer identification is one mix-up defense. Other deployments may use a different supported design, such as distinct redirect URIs per issuer. Authorization server mix-up compares the choices, and Checking the response issuer covers the validation rules for iss.
Keeping the protections distinct
In this example, state correlates the browser response with a session-bound transaction, PKCE binds code redemption to the verifier prepared for that transaction, and the issuer check verifies which authorization server answered.
PKCE can also stop the CSRF attack described earlier. The attacker's code is bound to the challenge from the attacker's own attempt, so when the printer redeems it with the verifier saved in your session, the exchange fails. Current guidance lets a client rely on that protection in place of state once it has confirmed that the authorization server enforces PKCE, because the protection depends on the server refusing a verifier that does not match. We retain explicit state in this teaching flow to make transaction handling clear. That does not make state, PKCE, and issuer validation interchangeable. As the Pixel Vault example showed, PKCE does nothing to stop a code being sent to the wrong server.
Finally, keep secrets and arbitrary return URLs out of state. An opaque random reference to server-side context is easier to control. Before returning the user to a page after connection, validate that destination independently so the callback cannot become an open redirect: an address on a trusted site that sends a browser wherever a link tells it to. Attackers use open redirects to make a link to their own page look as if it belongs to the trusted site.