THIS EXPLANATION
THE ROOM
CDA·170 Computing, Data & AI 6 MIN · 8 STATIONS

Proving you started the exchange you are finishing

A Socratic walk-through of proving you started the exchange you are finishing — reasoned out one step at a time, not lectured.

abcdefgh
a

The question we started with

THE QUESTION #

How do you stop someone else from completing a login you began?

A login that redirects you away and back has two halves separated by a gap. You begin it in one place; a short-lived authorization code comes back through your browser; something then redeems that code for a token.

The gap is the problem. The code travels through the browser, which is a leaky channel — redirect URLs reach history, logs, referrer headers, and on a phone, whichever application claimed the URL scheme. So ask the sharp question: if a code can be intercepted, what stops the interceptor from redeeming it? And can the answer be a secret, when the application redeeming it is a mobile or single-page app whose code anybody can download and read?

b

Reasoning it through

REASONING #

Start with why a secret does not obviously help. A server-side application can hold a client secret and present it at the token endpoint, so a stolen code is useless without it. A public client cannot: anything shipped inside an app binary or a JavaScript bundle is extractable, so it is not a secret and treating it as one is theatre.

But look again at what is really needed. We do not need a durable secret shared with the authorization server. We need to prove that the party redeeming the code is the same party that requested it, for this one exchange. That is a much weaker requirement, and weaker requirements admit cheaper solutions.

So consider generating a fresh random value at the start of every single login. Call it the verifier. It exists only in the memory of the app that made the request. Now: how do we let the authorization server later check it, without transmitting it over the leaky channel where it would be captured alongside the code?

This is the shape of a commitment. Send, on the outbound request, not the verifier itself but a one-way function of it — the SHA-256 hash, base64url-encoded, which the specification calls the code challenge and labels method S256. The authorization server stores that challenge with the code it issues. Later, on the direct back-channel call, the app presents the verifier. The server hashes it and compares. Match, token. No match, nothing.

Now check whether it works, from the interceptor's side. They have the code. They may well have seen the authorization request too, so they have the challenge. To redeem, they need a value that hashes to that challenge — which is asking them to invert SHA-256. That is the assumption the whole scheme rests on, and it is a well-tested one.

Notice the pleasing property: the secret was never transmitted until the moment it stopped being useful to anyone else. Commit first, reveal after the thing you were protecting has already been spent.

There is a footnote worth flagging, because it is a real failure mode. The specification also allows the plain method, where the challenge simply is the verifier. Against an attacker who sees only the redirect back, that still helps a little; against one who saw the original request, it protects nothing, because they read the verifier there. S256 is what the security is made of.

Now a boundary I want to draw carefully, because these two are constantly conflated. This mechanism proves the redeemer started the exchange. It does not by itself tell your browser session that the response coming back belongs to the request it sent — that is the job of the state parameter, which the client generates, sends out, and compares on return, and which defends against an attacker injecting their authorization response into your session. In practice a correctly implemented proof key also frustrates code injection, and OAuth 2.1 leans on it accordingly, but the two mechanisms answer different questions and a careful implementation still uses both.

One more update worth having right: this was introduced in 2015 for public clients that had no secret. Current best practice, in the OAuth 2.0 security guidance published as RFC 9700 in 2025 and in OAuth 2.1, is to use it for all clients, including confidential ones with secrets — because it defends against code injection, which a client secret does not.

c

The analogy

THE ANALOGY #
THE FIGURE

Sealing a prediction in an envelope before the race and handing the envelope to the steward, keeping the only copy of the wording to yourself. Anyone may hold the envelope; nobody can read what is inside, and nobody can claim your prize by guessing at the sealed text. When you come to collect, you say the words, the steward opens the envelope, and the two either match or they do not.

WHERE IT BREAKS DOWN

an envelope hides its contents, whereas a hash is published in the open — the challenge is visible to everyone and its protection comes not from concealment but from the impossibility of working backwards through the function.

d

Clarifying the model

THE MODEL #

Three refinements.

First, the verifier is not a password and must not behave like one. It is generated fresh per exchange, never reused, and discarded after the token arrives. Reuse across logins would recreate exactly the long-lived-secret problem the design was built to avoid.

Second, the protection is against interception in the leaky half. If an attacker has already compromised the device or the app process itself, they can read the verifier out of memory and this mechanism has nothing to say about it. Every defence has a threat model, and misplacing this one leads to overconfidence.

Third, the authorization server must actually enforce it: bind the challenge to the code at issuance, require a verifier at redemption when one was promised, and reject plain downgrades. A client that sends a challenge to a server that never checks it is protected by nothing at all — and a client cannot tell the difference from the outside.

e

A picture of it

THE PICTURE #
Proving you started the exchange you are finishing
Proving you started the exchange you are finishing Read left to right as one login, and track the verifier. It exists from the first stage but leaves the app only at the last, by which time the code it protects has already made its risky journey. The third stage is the attack: everything a thief can capture is on that stage, and it is not enough. The commitment is made before the danger and revealed after it. {"generator":"[email protected]","source":"../Socrates/.diagram-cache/_src/proving-you-started-the-exchange-you-are-finishing.md","sourceIndex":1,"sourceLine":4,"sourceHash":"18a5136009fb24c13491fd29284470e99033f03518de6ce92ca01a23a61f74b7","diagramType":"timeline","layoutVariant":"source","repairedDuplicateIds":[],"motion":"entrance-with-reduced-motion-fallback","presentation":"editorial","attempt":1,"viewBox":{"x":0,"y":0,"width":1154,"height":548},"qa":{"passed":true,"findings":[]}} Start App invents arandom verifier Only its hash goesout Consent User signs in at theserver Hash is bound tothe new code Return Code comes backvia the browser A thief gets thecode alone Exchange App reveals theverifier Hash must matchor no token

How to readRead left to right as one login, and track the verifier. It exists from the first stage but leaves the app only at the last, by which time the code it protects has already made its risky journey. The third stage is the attack: everything a thief can capture is on that stage, and it is not enough. The commitment is made before the danger and revealed after it.

f

What became clearer

WHAT CLEARED #
WHAT CLEARED

The exchange needed a link between its two halves, and the intuitive way to make one — a shared secret — is unavailable to any client whose code the public can read. Committing to a random value with its hash supplies the link anyway: it is unforgeable, it costs nothing to create, it is thrown away immediately, and it turns a stolen authorization code from a credential into a useless string.

g

Where to go next

ONWARD #
  • What state defends against that a proof key does not, and why both remain recommended.
  • Sender-constrained tokens such as DPoP, which extend the same idea past the code exchange to the token itself.
  • Why custom URI schemes on mobile were the interception vector that motivated this design.
h

Key terms

TERMS #
TermWhat it means
PKCE (Proof Key for Code Exchange)RFC 7636, the mechanism binding an authorization code to the client instance that requested it.
Code verifiera high-entropy random string generated per authorization request and revealed only at the token endpoint.
Code challengethe value derived from the verifier and sent on the authorization request, normally its SHA-256 digest under method S256.
Public clienta client that cannot hold a credential confidentially, such as a mobile or browser-based application.
state parametera client-generated value echoed back on the redirect, used to tie the response to the request the client's own session made.

Every term the collection defines is gathered in the glossary.

Nearby on the shelf

4