Technical explanations · Drawbly
OAuth PKCE: why is a stolen code not enough?
By Drawbly ·
In an OAuth authorization-code flow, a browser app receives a short-lived code and exchanges it for a token. If another app gets that code first, can it make the same exchange? Proof Key for Code Exchange (PKCE) adds a per-request secret that makes the stolen code alone insufficient.

One fictional calendar connection
Imagine a browser-based task board that asks a calendar provider for permission to read events. The task board is a public client: code shipped to a browser cannot keep a long-term client secret confidential. This is a teaching example, not a claim about a real provider or Drawbly integration. The task board starts one authorization request; the provider's authorization server later sends an authorization code to the registered redirect URI.
RFC 10017 requires browser-based public clients to use PKCE with the authorization-code grant, and requires authorization servers to support and enforce it for those clients. The general OAuth security best current practice also recommends PKCE for confidential clients. The flow below explains the proof, not a complete OAuth implementation.
First, commit to a verifier without sending it
- Create a new verifier. The task board generates a high-entropy random
code_verifier, shown asVin the drawing. It creates a fresh value for this authorization request and retains it for the return trip. RFC 7636 specifies the verifier format, including a 43–128-character range. - Send a derived challenge. The task board derives
H(V) = BASE64URL(SHA256(ASCII(V))). It sends thiscode_challengeandcode_challenge_method=S256in the authorization request, together with ordinary OAuth parameters such as client ID, redirect URI and scope. It does not putVin that request. - Bind the code. After the user authorizes, the authorization server issues fictional code
Cand associates it with the challenge and method. The browser is redirected back to the registered callback withC. The drawing leaves the consent screen and redirect details out so the proof remains readable.
Why S256? If an observer sees the authorization request, the challenge should not reveal the verifier. RFC 7636 defines the S256 transform, while RFC 9700 recommends a method that does not expose the verifier. The plain method sends the verifier itself as the challenge and does not provide that property.
Same code, two different token requests
Legitimate path: the task board sends C and its saved V to the token endpoint. The authorization server recomputes H(V) and compares it with the challenge bound to C. If they match, it continues the normal checks before issuing a token. A match is necessary here; it is not a promise that every other authorization check succeeds.
Intercepted-code path: suppose another app obtains C from the redirect but never learns V. A request containing only C cannot pass the PKCE check. Guessing a high-entropy verifier is impractical; a wrong verifier produces a different challenge. RFC 7636 requires the token endpoint to reject a mismatch with invalid_grant. This is the narrow protection the diagram shows: an intercepted code cannot be redeemed without the matching verifier.
What does PKCE not cover?
If an attacker steals both the code and the verifier, PKCE alone cannot make that pair safe. It also does not protect a token after issuance or replace registered redirect URI checks. Browser code needs protection against script injection and unsafe token handling. RFC 10017 separately requires a CSRF defense at the redirect URI; enforced PKCE may serve that role in the stated conditions, or the app can verify a unique OAuth state value (or suitable OpenID Connect nonce). Do not infer that adding a challenge alone completes the authorization flow.
The verifier is also different from a client secret. A client secret identifies a confidential client and must remain on a server that can keep it private. PKCE binds this authorization attempt to a verifier that the initiating client instance holds. It is generated again for the next attempt. If the provider silently ignores the challenge or permits a downgrade, the intended protection is lost; confirm support and enforcement rather than assuming it from a successful redirect.
Draw the trust boundary you need to explain
For a design review, label where the verifier is created and held, where the challenge is stored, and where the comparison happens. If you need to discuss UI consent, redirects and tokens, add those to your copy of the editable drawing. The portrait drawing lays out the same proof vertically. For a different authorization boundary, see the MCP client and server tool-call example; for retry behavior after a token-backed API call, see the idempotency example.
RFC 7636: PKCE, RFC 9700: OAuth 2.0 Security Best Current Practice, and RFC 10017: OAuth 2.0 for Browser-Based Applications. The task board, code C and verifier label V are fictional shorthand.