A native application starts an OAuth authorization flow in the system browser, then waits for the operating system to route the redirect back to the app. The authorization endpoint is protected by TLS, yet the returned authorization code crosses a different boundary: application dispatch on the local device. Another application able to receive that redirect may obtain the code before the intended client does.

Proof Key for Code Exchange, or PKCE, changes the value of that intercepted code. The client creates a transaction-specific secret called the code_verifier, sends only a derived code_challenge in the authorization request, and later presents the verifier when redeeming the code. The authorization server binds the challenge to the issued code. Possession of the code alone is then insufficient for redemption.

The security property depends on that binding surviving the complete transaction. A server that accepts the verifier but fails to associate PKCE state with the authorization code has not implemented the mechanism. A client that reuses verifier material across transactions also weakens the isolation PKCE is designed to create.

The authorization code gains a second redemption condition

In a basic authorization-code flow, the code is a short-lived credential presented to the token endpoint. Other controls still apply, including client authentication where the client has credentials, redirect URI checks required by the protocol profile, and the authorization server’s own code validation rules.

PKCE adds another condition. Before sending the authorization request, the client generates a high-entropy code_verifier. RFC 7636 defines the verifier as a string from 43 to 128 characters using the unreserved URI character set. The verifier is retained by the client for the pending transaction.

For the S256 method, the challenge is derived as:

BASE64URL-ENCODE(SHA256(ASCII(code_verifier)))

The client sends that challenge and the S256 method identifier to the authorization endpoint. The authorization server records the challenge and method in association with the code it later issues. The association can be represented in server-side state or encoded into protected code state; the storage design is not the security boundary. What matters is that a token request cannot detach the code from the challenge recorded for its authorization transaction.

At the token endpoint, the client sends the authorization code plus the original verifier. The server applies the recorded transformation and compares the result with the bound challenge. A mismatch causes the grant to fail.

This turns the authorization code from a credential redeemable by its bearer into a credential whose redemption also requires possession of per-request material retained by the client.

S256 keeps the verifier out of the front channel

PKCE defines both plain and S256, but they expose different properties when authorization-request data can be observed.

With plain, the challenge is the verifier itself. An observer that can obtain the authorization request can therefore obtain the material needed to satisfy the PKCE check. RFC 7636 retains plain for compatibility, while recommending cryptographically secure challenge methods and specifically S256 where available.

With S256, the authorization request carries a SHA-256-derived value rather than the verifier. An actor that sees the challenge cannot directly substitute that value as the verifier because the token endpoint hashes the submitted verifier before comparison.

The verifier still requires protection. If malicious software can read the client’s stored verifier, PKCE does not make that disclosure harmless. The mechanism separates the redirect-channel artifact from the redemption secret; it does not provide a hardware-backed secret store or isolate a compromised client process.

RFC 9700 narrows current deployment expectations further. It states that authorization servers must support PKCE and that clients using PKCE should use a challenge method that does not expose the verifier in the authorization request. At present, S256 is the method identified for that property.

Transaction binding blocks code substitution

Code interception is the original PKCE threat model, but the same transaction binding also matters when an authorization response contains a code from a different flow.

Suppose a client has started transaction A and retained verifier A. An attacker obtains a code created in transaction B, where that code is bound to challenge B, and injects it into the redirect arriving for transaction A. The client attempts redemption using verifier A. A conforming authorization server derives challenge A from that verifier and compares it with challenge B stored against the injected code. The mismatch causes redemption to fail.

That property is stronger than checking that a verifier-shaped parameter exists. The token endpoint has to verify the specific verifier associated with the specific authorization request that produced the code.

RFC 9700 also describes a downgrade case for deployments that permit both PKCE and non-PKCE flows. If a token request includes a code_verifier but the corresponding authorization request had no code_challenge, accepting the request as an ordinary non-PKCE exchange can defeat the client’s expectation that PKCE protected the transaction. Current security guidance requires the authorization server to reject that condition.

Mandating PKCE for the relevant clients removes this optional-path ambiguity. Where compatibility still permits non-PKCE transactions, the server must retain enough state to distinguish a code issued with a challenge from one issued without one and enforce the correct redemption semantics.

PKCE does not replace every OAuth binding

The PKCE verifier proves possession of transaction-specific material. It does not, by itself, establish every identity or routing property involved in OAuth.

A confidential client can still authenticate at the token endpoint using its configured client authentication method. PKCE and client authentication protect different boundaries: one binds a code to a particular authorization transaction, while the other can authenticate the client according to the authorization server’s client model. RFC 9700 recommends PKCE for confidential clients as well as public clients because code injection and misuse are not confined to applications that lack a client secret.

Redirect URI validation also remains significant. Authorization servers need strict redirect controls so authorization responses are delivered only to registered destinations permitted for the client. PKCE reduces the value of a stolen code when the attacker lacks the verifier; it does not convert an arbitrary redirect target into a safe configuration.

The state parameter has its own role in OAuth deployments. Current security guidance permits clients that have ensured authorization-server PKCE support to rely on PKCE for CSRF protection in the described redirect-flow conditions. Other protocol profiles can impose additional requirements, and OpenID Connect introduces nonce with separate validation semantics. Treating these fields as interchangeable tokens obscures the distinct values each one binds.

PKCE also does not sender-constrain the access token produced after successful redemption. Once the authorization server issues a bearer access token, possession rules for that token are governed by the token type and any separate sender-constraining mechanism. PKCE protects the authorization-code exchange; it does not automatically bind later API calls to the verifier.

Native redirect routing remains an exposed boundary

PKCE was created around a practical native-application problem. A browser can protect its connection to the authorization server with TLS while the final redirect is handed to local application-routing machinery outside that TLS connection.

Custom URI schemes can be especially relevant to this boundary because more than one application may be able to register or claim handling for a scheme, depending on platform behavior. RFC 8252 requires public native clients to use PKCE and describes redirect patterns intended for native applications.

PKCE does not need the operating system to prove that the intended app exclusively received the code. Instead, it makes interception at that boundary insufficient for token redemption unless the interceptor also obtains the verifier.

That distinction matters operationally. Redirect ownership controls and PKCE are complementary. Stronger platform association for an application redirect can reduce opportunities for interception, while PKCE limits the consequence when the authorization code is exposed separately from verifier state.

Server state is part of the security boundary

An authorization server can implement PKCE without storing a conventional database row for every pending code. It can place protected transaction state inside an authorization code or reference server-side records. Either design can satisfy the protocol if the code challenge and method remain integrity-protected and inseparable from the issued code.

Implementation shortcuts can break that property. A token endpoint must not trust a challenge supplied anew by the client during redemption. Doing so would let the party presenting a stolen code choose the value against which its own verifier is checked. The relevant challenge is the one received at the authorization endpoint and bound to that code before issuance.

The same requirement applies across distributed authorization infrastructure. If the authorization endpoint and token endpoint run on separate services or regions, the PKCE state must cross that internal boundary with integrity. A cache miss cannot safely degrade into skipping verification for a code that was issued with PKCE.

Authorization-code lifetime and one-time-use rules still matter as independent controls. PKCE does not justify making codes reusable or indefinitely valid. Its contribution is narrower: code possession and verifier possession must converge at redemption for the same transaction.

Failure handling preserves the binding

A PKCE mismatch is not a recoverable hint that should trigger a second redemption attempt without PKCE. RFC 7636 specifies an invalid_grant error when verifier validation fails. Falling back to a weaker path after a mismatch would turn protocol compatibility into a downgrade mechanism.

Clients likewise should not retry with plain merely because an S256 request fails. RFC 7636 states that clients must not downgrade from S256 to plain after such an error. A server that supports PKCE is required to support S256; failure is therefore not a signal to expose the verifier in the authorization request.

The resulting trust boundary is precise. The browser-facing authorization request commits the transaction to a challenge. The redirect carries a code that references that commitment. The back-channel token request reveals the verifier. Security comes from enforcing that sequence as one bound transaction, not from any single parameter in isolation.