What Is PKCE and Why Does It Matter
PKCE prevents stolen OAuth authorization codes from being exchanged for access tokens. Learn how it works, the attack it blocks, and why modern OAuth flows require it.
PKCE prevents stolen OAuth authorization codes from being exchanged for access tokens. Learn how it works, the attack it blocks, and why modern OAuth flows require it.
PKCE (Proof Key for Code Exchange, pronounced “pixy”) is a security extension to OAuth 2.0 that stops attackers from stealing authorization codes and trading them for access tokens. It creates a one-time cryptographic binding between the authorization request and the token exchange, so only the app that started the login can finish it. It was built for mobile apps and single-page applications that can’t safely hold a secret, and it’s now recommended for every OAuth client. In OAuth 2.1 it becomes mandatory.
This article covers how PKCE works, the attack it shuts down, the parameters that make it tick, implementation best practices, and where it fits across client types. If you’re wiring OAuth into an app, you want this on by default. For the broader protocol picture, our guide on when to use OAuth 2.0 and OpenID Connect covers the decisions PKCE sits underneath.
PKCE is a security extension to OAuth 2.0 that prevents attackers from stealing authorization codes and exchanging them for access tokens. It works by creating a one-time cryptographic link between the authorization request and the token exchange, ensuring that only the application that initiated the login flow can complete it.
RFC 7636 introduced PKCE in 2015 to solve a specific problem: mobile apps and browser-based applications can’t safely store secrets. Any credential baked into client-side code can be pulled out by anyone with access to the device or browser. PKCE sidesteps that entirely by generating a fresh, random secret for each authorization request. For the public-client version of this problem, see our walkthrough of OAuth 2.0 for mobile apps, SPAs, and browsers.
OAuth 2.0’s Authorization Code flow was designed with server-side web apps in mind. In that model, the application swaps an authorization code for tokens using a client secret that never leaves the server. The authorization server verifies the secret before issuing tokens, thereby proving the request came from the real application. For the server-side version of this setup, see how teams run an open-source OAuth2 server for API security.
Mobile apps and SPAs break that model. Any secret stored in an app binary or JavaScript bundle can be extracted with reverse engineering or browser dev tools. Without a reliable way to prove identity, what stops an attacker from grabbing the authorization code and exchanging it themselves? That’s exactly what the authorization code interception attack exploits.
Here’s how it plays out. A user installs a legitimate banking app. They also install a malicious app disguised as a game. Both register the same custom URL scheme, like mybank://callback. When the user logs in through the banking app, the authorization server redirects back with an authorization code, and the operating system, seeing two apps registered for that scheme, may hand the redirect to the malicious app instead.
Without PKCE, the attacker has everything they need. The code is valid and there’s no client secret to verify against. PKCE closes the gap by requiring proof that the token request came from the same application that started the authorization request.
PKCE adds a cryptographic handshake to the authorization code flow. The client generates a secret, sends a transformed version of it with the authorization request, then proves knowledge of the original secret during the token exchange. An attacker who intercepts the code can’t complete the exchange because they never saw the original secret.
The client generates a code verifier, a cryptographically random string between 43 and 128 characters using A-Z, a-z, 0-9, and the symbols -._~. This value stays on the client and isn’t transmitted until the final token exchange. A verifier should be generated fresh for every request and never reused; for illustration, it may look like: dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk.
The client creates a code challenge by hashing the verifier with SHA-256 and base64url-encoding the result: code_challenge = BASE64URL(SHA256(code_verifier)). The transformation is one-way, so you can’t reverse the challenge back into the verifier, which makes the challenge safe to send to the authorization server.
The client includes two extra parameters in the authorization request: code_challenge (the derived value) and code_challenge_method=S256 (indicating SHA-256). The authorization server stores both alongside the authorization code it generates.
When the client trades the authorization code for tokens, it sends the original code_verifier to the token endpoint. This is the first and only time the verifier leaves the client.
The authorization server hashes the received verifier with SHA-256 and compares it to the stored challenge. Match issues tokens. No match, and the request fails even if the authorization code itself is valid. An attacker who intercepted the code holds the challenge from the original request but can’t derive the verifier from it because the SHA-256 hash is computationally irreversible.
Three parameters make PKCE work. Understanding each one helps when you’re implementing OAuth flows or debugging an auth integration.
A high-entropy random string the client generates fresh for each authorization request. RFC 7636 specifies a character set (A-Z, a-z, 0-9, -._~) and a length of 43 to 128 characters. A 43-character verifier delivers roughly 256 bits of entropy, putting brute force out of reach. The verifier stays secret until the token exchange. It’s never in a URL, never logged, never sent during the initial request.
The transformed version of the verifier. For S256, that’s SHA-256 hashing followed by base64url encoding. The result is a fixed-length string you can transmit safely because it can’t be reversed into the verifier.
RFC 7636 defines two methods for deriving the challenge:
| Method | Transformation | Security | When to Use |
|---|---|---|---|
| S256 | SHA-256 hash + base64url encoding | Strong | Default for all clients |
| plain | None (verifier equals challenge) | Weak | Only when SHA-256 is unavailable |
The plain method sends the verifier directly as the challenge, which offers no cryptographic protection. Intercept the request, and you have the verifier and can complete the exchange. It exists only for constrained environments that can’t hash, which is rare given that SHA-256 ships on virtually every modern platform.
See how Ory Hydra enforces PKCE out of the box
OAuth 2.0 sorts clients by whether they can keep a secret:
PKCE was built for public clients, which have no other way to prove identity during the token exchange. A mobile app can’t rely on a client secret because any secret in the binary can be extracted. But the OAuth 2.0 Security BCP now recommends PKCE for confidential clients too. The reason is authorization code injection, a different attack where an attacker tricks a legitimate client into exchanging a stolen code. A client secret alone can’t stop it because the attacker is using the legitimate client to do the exchange. PKCE does stop it because the stolen code is bound to a different verifier. For the enterprise angle on this, see when to use OAuth2 for scaling enterprise authentication and authorization.
OAuth 2.0 treats PKCE as optional but strongly recommended. OAuth 2.1 changes that, making PKCE mandatory for all authorization code flows regardless of client type.
The OAuth 2.1 draft consolidates the security best practices that emerged since 2012. PKCE is one of several requirements becoming mandatory, reflecting consensus that the protection it buys outweighs the implementation cost. If you’re standing up an infrastructure, you can run an OAuth2 and OpenID Connect server with Ory Hydra with PKCE enforcement built in.
S256 provides actual cryptographic protection. Plain does not. With S256, the challenge reveals nothing about the verifier, so intercepting the request provides no advantage to an attacker. With plain, the challenge equals the verifier, so intercepting the request hands the attacker everything. The only reason to use plain is a platform that truly can’t run SHA-256, and given that SHA-256 ships in JavaScript, iOS, Android, and essentially every modern runtime, that’s exceptionally rare.
The verifier’s security depends on the randomness of its inputs. Use a cryptographically secure RNG, not timestamps, user IDs, or counters. Most platforms provide the right API: crypto.getRandomValues() in browsers, SecureRandom in Java, os.urandom() in Python. A 43-character verifier gives roughly 256 bits of entropy, more than brute force can chew through.
The verifier lives only for the duration of the flow, usually seconds to a few minutes. Keep it in memory or platform-specific secure storage. Don’t write it to logs, don’t persist it to disk unnecessarily, and don’t put it in URLs where it could land in browser history or server access logs.
PKCE protects against authorization code interception and injection. It doesn’t replace the other OAuth security mechanisms. PKCE also does not replace exact redirect URI validation; the authorization server still needs to strictly validate redirect URIs. For the wider catalog of what you’re defending against, see our rundown of common authentication attack vectors.
A complete implementation uses all three together.
As AI agents and automated systems call APIs on behalf of users and organizations, the principles behind PKCE get more relevant, not less. An agent requesting access tokens faces the same risks as a mobile app: the authorization code can be intercepted, and the agent can’t necessarily safely store a long-lived secret. Binding each authorization request to its token exchange provides auditability and prevents unauthorized token acquisition, even when agents operate at high volume and speed. Ory’s work on identity for agentic AI situates PKCE within the broader agent identity stack.
Fine-grained access control for machine identities depends on knowing that each token was legitimately obtained by the requesting entity. For the agent-specific patterns built on top of this, see our guide to MCP server authentication with Ory Hydra.
Ory Hydra is an OAuth 2.0 and OpenID Connect provider with built-in PKCE support. Hydra automatically enforces PKCE validation when clients include the required parameters, and it can be configured to require PKCE for specific clients or all clients. For the wider authorization picture, Ory Keto adds fine-grained, Zanzibar-style access control on top of the token layer.
Ory ships multiple deployment options: open-source self-hosted, an enterprise license for production, and Ory Network as a fully managed service. Each one supports PKCE out of the box, alongside the broader OAuth 2.0 and OpenID Connect specifications modern applications depend on.
Explore Ory Hydra, the OAuth 2.0 server
Does PKCE Replace the Client Secret
For public clients, PKCE provides security without a client secret. For confidential clients, the two serve different purposes and work together: the secret proves the client’s identity, PKCE prevents authorization code injection. Using both is defense in depth.
Can PKCE Be Used With Refresh Tokens
Yes. PKCE secures the initial exchange of authorization codes. Once the client holds tokens, refresh handling follows the standard OAuth 2.0 process, with its own considerations such as rotation and expiration.
What Attacks Does PKCE Not Protect Against
PKCE addresses authorization code interception and injection. It doesn’t protect against phishing, token theft after issuance, cross-site scripting, or a compromised authorization server. A complete security strategy handles each of those separately.
Is PKCE Part of OAuth 2.0 or OpenID Connect
PKCE is defined in RFC 7636 as an extension to OAuth 2.0. It works with standalone OAuth 2.0 and with OpenID Connect, which builds on OAuth 2.0. Most modern identity providers support it, and many enable it by default for public clients. For how these specs fit together, our guide on OAuth 2.0 and OpenID Connect use cases is a good next read.
Run OAuth 2.0 and OpenID Connect with PKCE on Ory
PKCE isn’t optional engineering hygiene anymore. It’s the default. Generate a high-entropy verifier; always use S256; keep the verifier out of logs and URLs; and pair it with the state and nonce. Turn it on for confidential clients too, because the injection attack doesn’t care whether you have a client secret. OAuth 2.1 will require all of this anyway. The teams building it in now are the ones who won’t be retrofitting their auth layer under deadline pressure later. If you want it enforced for you, Ory Hydra and Ory Network do it out of the box.