An OAuth authorization code can pass through a browser, an operating-system URL dispatcher, a custom application scheme, or an application link before it reaches the client that requested it. That route is convenient, but it also means possession of the returned code is not always strong evidence that the intended client received it.

Proof Key for Code Exchange, usually called PKCE, changes the value of an intercepted code. Before starting the authorization request, the client creates a high-entropy secret called the code verifier. The authorization server receives a derived code challenge with the request and later requires the original verifier when the code is exchanged for tokens. A party that captures only the authorization code lacks the second value needed to complete the exchange.

The mechanism is compact, but its security value comes from a precise binding: a particular authorization request, the code issued from it, and a secret held by the initiating client become part of one transaction.

The authorization code is not the final credential

The authorization-code flow deliberately separates browser-facing authorization from the token endpoint. After authentication and consent, the authorization server redirects the user agent with a short-lived code. The client then presents that code to the token endpoint and receives tokens if the exchange satisfies server policy.

Without an additional client-held proof, interception can matter when another application can receive or observe the redirect. Native applications historically made this especially visible because custom URI schemes can have weaker ownership properties than an HTTPS origin. Platform controls such as claimed HTTPS links improve routing assurance, but PKCE addresses a different part of the transaction: it makes the code insufficient on its own.

The distinction is important. PKCE does not encrypt the authorization code and does not prevent another process from seeing it. It changes what an observer can do with that code.

A verifier created before the redirect establishes continuity

The client generates a code verifier before sending the browser to the authorization endpoint. For the commonly used S256 method, the client hashes that verifier with SHA-256 and encodes the result using base64url without padding. That derived value becomes the code challenge sent in the authorization request.

The authorization server associates the challenge and challenge method with the authorization transaction. When the resulting code reaches the token endpoint, the client supplies the original verifier. The server derives the challenge again and compares it with the value bound to the code. A mismatch causes the exchange to fail.

This ordering matters. The secret is chosen before the browser-facing part of the flow, while the proof is presented after the redirect. An interceptor cannot repair a missing verifier by substituting a new one at the token endpoint because the server already committed the code to the earlier challenge.

The S256 method is preferable to the plain method because the authorization request exposes only a one-way transformation of the verifier. Current OAuth security practice expects clients to use PKCE and authorization servers to support it, with S256 as the normal challenge method.

PKCE and state protect different relationships

PKCE is sometimes treated as a replacement for every correlation control around an OAuth redirect. It is not. The controls answer different security questions.

The state parameter is commonly used to correlate the authorization response with browser-side application state and to defend redirect endpoints against cross-site request forgery patterns. PKCE binds the authorization code to possession of the verifier held by the client. OpenID Connect flows can also use a nonce to bind identity assertions to a client transaction.

A robust implementation preserves these distinctions instead of compressing them into a single generic token. Each value has its own scope, storage requirements, and validation point. Combining controls conceptually can produce gaps when a framework validates one relationship but developers assume it covered another.

PKCE also does not compensate for permissive redirect URI registration. If an authorization server accepts loosely matched redirect destinations, an attacker may be able to steer responses toward an unintended endpoint. Exact redirect URI matching and PKCE reinforce separate boundaries: one constrains where the response may go, while the other constrains who can redeem the code.

Public clients are the clearest case, not the only case

PKCE was introduced to address authorization-code interception for public clients, which cannot safely hold a static client secret. A mobile application distributed to many devices is the classic example: embedding a shared secret in the application package does not turn that value into a confidential credential.

PKCE avoids relying on such a static secret. The verifier is fresh per authorization transaction and exists only for the duration of that flow. Capturing one verifier should not grant authority over later transactions.

The same property is useful for confidential clients as well. A backend application can authenticate itself to the token endpoint and still use PKCE to bind the code to the initiating transaction. Client authentication establishes the identity of the client software or deployment; PKCE establishes possession of the per-transaction verifier. Those are complementary claims.

This distinction also limits the damage from treating a client secret as a universal answer to code interception. A valid client credential does not prove that a particular authorization code belongs to the browser transaction currently being completed.

Storage mistakes can erase the intended binding

PKCE depends on the verifier remaining available to the legitimate client and unavailable to an interceptor until the token exchange completes. That sounds straightforward, but modern applications often distribute authorization state across browser tabs, backend nodes, mobile processes, and temporary storage layers.

A verifier stored in a broad shared namespace can be paired with the wrong transaction. A verifier written to telemetry can become visible to systems and operators that never needed it. A verifier placed in a URL can leak through browser history, intermediary logs, analytics, or referrer data. The value should be handled as transient security-sensitive state rather than ordinary request metadata.

Concurrency also matters. A client can have several authorization attempts in flight at once. Mapping every callback to one global verifier creates race conditions and accidental cross-binding. The application needs transaction-specific state so the callback resolves to the verifier created for that authorization request.

Authorization servers have an equally important obligation: the challenge must be bound to the code when the authorization transaction is created and enforced when that code is redeemed. Accepting a verifier only when one happens to be supplied, or permitting a request that began with PKCE to finish without it, turns a mandatory proof into an optional hint.

PKCE narrows one interception path, not the whole OAuth threat model

A successful verifier check says something specific: the party redeeming the code possesses the secret corresponding to the challenge attached to that authorization transaction. It does not establish that every surrounding component is trustworthy.

Malicious redirect destinations, compromised browsers, stolen refresh tokens, authorization-server compromise, weak client-side session handling, and consent manipulation sit outside that guarantee. So do application bugs that attach the resulting tokens to the wrong local account after a valid exchange.

That bounded guarantee is a strength rather than a weakness. Security mechanisms are easier to compose when their claims are narrow and explicit. PKCE does not need to solve every OAuth failure mode to remove an important assumption from the authorization-code flow: possession of a code no longer has to be treated as sufficient proof that the intended client received it.

The operational standard is therefore less about adding two parameters and more about preserving the transaction binding end to end. Generate a fresh verifier, derive the expected challenge, bind it to the authorization request, retain the verifier in transaction-scoped state, and require the match at redemption. If any of those links becomes optional, the flow can quietly return to bearer-code semantics at the exact boundary PKCE was designed to strengthen.