OAuth PKCE: Binding Authorization Codes to the Client That Asked for Them
PKCE turns an intercepted authorization code into a dead end by binding it to a client-generated secret. Here is how the flow works, where enforcement still varies, and how to check your server.
31 Oct 2025, 21:07 UTC

OAuth PKCE: Binding Authorization Codes to the Client That Asked for Them
If you ship a mobile app or a single-page application, you already know the pain: OAuth 2.0 was designed for confidential clients that can guard a secret, but your app runs on a device or in a browser where any string you embed can be extracted. The Authorization Code Grant was never enough on its own. Proof Key for Public Clients (PKCE, RFC 7636) closes that gap by binding the authorization code to the specific client instance that requested it. The result is simple: an intercepted code becomes useless without the matching verifier.
The interception problem PKCE actually solves
In a plain authorization code flow, the client redirects the user to the authorization server, receives a short-lived code on a redirect URI, and exchanges that code for tokens. On a mobile device, another app can register the same custom-scheme redirect URI and siphon the code. In a browser, cross-site scripting or a malicious extension can observe the redirect. Once the code is stolen, the attacker can replay it at the token endpoint.
PKCE adds two client-generated values. The client creates a high-entropy code_verifier—a random string of 43–128 characters. It then derives a code_challenge by hashing the verifier with SHA-256 and Base64URL-encoding the result. The authorize request carries the challenge and the method (S256). The authorization server stores that challenge and issues the code. Later, the token request must include the original verifier. The server hashes it and compares. If they match, the code is redeemed; if not, the request fails.
The key property is proof-of-possession. The verifier never leaves the client until the token exchange, so an attacker who intercepts the code cannot complete the flow.
How the binding works in practice
The flow adds exactly two steps to the standard authorization code dance: generate the verifier/challenge pair before the authorize request, and present the verifier during the token request. There is no server-side registration required, which is why PKCE works for public clients that cannot hold a client secret.
A critical detail is the challenge method. S256 applies SHA-256 to the verifier. The plain method sends the verifier itself as the challenge, which offers no protection against an attacker who can read the authorize request. Authorization servers increasingly reject plain for public clients, and you should treat S256 as mandatory.
Worked example: generating the pair and testing enforcement
You can reproduce the mechanics with standard command-line tools. Run these in a terminal on your workstation; no elevated permissions are needed.
First, generate a verifier and derive its S256 challenge:
# 1. Create a high-entropy verifier (43–128 chars, Base64URL-safe)
VERIFIER=$(openssl rand 32 | base64 | tr -d '\n' | cut -c1-43)
# 2. Derive the S256 challenge
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -binary -sha256 | base64 | tr '+/' '-_' | tr -d '=')
Next, send the user to the authorize endpoint. In a real app this is a browser redirect; the URL looks like this:
https://auth.example.com/authorize?
response_type=code&
client_id=mobile-app&
redirect_uri=com.example.app%3A%2Foauth&
code_challenge=CHALLENGE_VALUE&
code_challenge_method=S256&
state=random-state
Replace CHALLENGE_VALUE with the value stored in $CHALLENGE. The state parameter remains important for CSRF protection and is orthogonal to PKCE.
After the user authenticates, your redirect handler receives an authorization code. Exchange it for tokens by presenting the verifier:
curl -X POST https://auth.example.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=AUTH_CODE_FROM_REDIRECT \
-d redirect_uri=com.example.app%3A%2Foauth \
-d client_id=mobile-app \
-d code_verifier=$VERIFIER
Run this from your terminal or backend service. The client_id identifies your public client, and AUTH_CODE_FROM_REDIRECT is the placeholder for the code your app just received. Do not log the verifier in production; it is a bearer credential for this single exchange.
Enforcement gaps and the reuse trap
PKCE is only as strong as the server that enforces it. Enforcement varies across authorization servers, and some legacy endpoints may not reject requests that omit the verifier entirely. If your token endpoint accepts a code without a code_verifier, PKCE provides no protection. Treat this as a deployment requirement, not an optional feature.
A second pitfall is verifier reuse. Generating one verifier and recycling it across multiple authorization requests invalidates the protection and may trigger security alerts. Each authorization attempt should use a fresh, high-entropy verifier. Store it in memory only for the duration of that flow; do not persist it to disk or shared preferences.
There is also a boundary to what PKCE solves. It prevents code interception, but it does not stop a phishing site that tricks the user into authenticating and handing over the resulting tokens. It also does not replace client authentication for confidential clients; a backend service exchanging a code should still use its client secret or private-key JWT in addition to PKCE.
Verify your server actually enforces it
The most practical check is a negative test. Repeat the token request above but deliberately omit the code_verifier:
curl -X POST https://auth.example.com/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=AUTH_CODE_FROM_REDIRECT \
-d redirect_uri=com.example.app%3A%2Foauth \
-d client_id=mobile-app
The server should refuse the request with an OAuth error—commonly invalid_grant or invalid_request—rather than issuing tokens. If it returns an access token, your server is not enforcing PKCE for that client, and you should raise it with your identity provider or upgrade your authorization server.
For a broader check, use an OAuth interop test suite or tool such as the OAuth 2.0 Playground to confirm that your server mandates PKCE for public clients and rejects the plain challenge method. These tools exercise the spec edge cases without requiring you to build a full client.
PKCE is now a baseline expectation for native and single-page applications, not an advanced option. The implementation cost is two extra steps and a few lines of code; the benefit is that a stolen authorization code becomes a dead end. Generate a fresh verifier every time, send S256, and verify that your token endpoint rejects requests that skip the proof.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.