Using PKCE to Secure Public Clients in OAuth 2.0 Authorization Code Flow
Learn how PKCE adds a verifier/challenge pair to the OAuth 2.0 authorization code flow, protecting public clients from authorization‑code interception without requiring a client secret.
24 Jul 2025, 10:30 UTC

Why public clients need extra protection
When a client cannot keep a secret—such as a single‑page app or a native mobile app—the authorization code returned from the authorization endpoint is visible to anyone who can intercept the redirect. An attacker who steals the code can exchange it for an access token unless the authorization server ties the code to something only the legitimate client knows. This is the problem PKCE (Proof Key for Code Exchange) solves.
How PKCE binds the authorization code to a secret verifier
PKCE adds two parameters to the flow: a code_verifier that the client creates and keeps secret, and a derived code_challenge sent with the authorization request. The server stores the challenge and, when the client later exchanges the authorization code for a token, it recomputes the challenge from the verifier supplied in the token request. A mismatch causes the server to reject the request with invalid_grant, preventing use of a stolen code.
Generating the verifier and challenge
The verifier must be a cryptographically random string of 43‑128 characters (octets) using the unreserved URL‑safe alphabet. A common way to produce it is to generate random bytes and base64url‑encode them without padding.
# Example: create a verifier and its challenge (run in a Unix‑like shell)
VERIFIER=$(head -c 32 /dev/urandom | base64url) # 32 bytes → 43‑char base64url
CHALLENGE=$(echo -n "$VERIFIER" | openssl dgst -sha256 -binary | base64url)
The base64url step omits the ‘=’ padding characters; many libraries provide a helper that does this automatically.
Token endpoint verification
When the client posts to the token endpoint it includes the original code_verifier. The authorization server:
- Decodes the verifier (ensuring it is 43‑128 chars).
- Computes SHA‑256 of the verifier.
- Base64url‑encodes the hash without padding.
- Compares the result to the stored
code_challenge. - If they match, the authorization code is validated; otherwise the request is rejected.
Worked example: end‑to‑end flow
Below is a simplified, illustrative sequence showing the values that would be exchanged. Replace the placeholders with actual values from your environment.
# 1. Client creates verifier and challenge
VERIFIER=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk # 43‑char example
CHALLENGE=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
# 2. Authorization request (browser redirect)
# https://auth.example.com/authorize?response_type=code&client_id=myapp&redirect_uri=https%3A%2F%2Fmyapp.example.com%2Fcallback&scope=read&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256
# 3. After user authenticates, the browser is redirected to:
# https://myapp.example.com/callback?code=AUTH_CODE_123&state=...
# 4. Token request (client‑side, no secret)
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_123' -d 'redirect_uri=https%3A%2F%2Fmyapp.example.com%2Fcallback' -d 'client_id=myapp' -d 'code_verifier=dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk'
# 5. Authorization server verifies the challenge; if correct it returns:
# {'access_token':'...','token_type':'Bearer','expires_in':3600}
Limitations and practical checks
- Single‑use: A verifier must be used only once; reusing it across multiple token requests causes compliant servers to reject the request.
- Length limits: Values outside the 43‑128 octet range are rejected outright, so client libraries must validate the generated bytes.
- Encoding mismatch: If the client adds padding (‘=’) while the server expects base64url without padding, the challenge verification fails even though the underlying bytes are correct.
- Entropy: Low‑entropy verifiers (e.g., a counter) are guessable and weaken the protection; use a cryptographically secure random source.
To verify that your implementation is working:
- Generate a verifier and compute the challenge locally.
- Compare the locally computed challenge with the value you sent in the authorization request; they must match.
- Attempt a token request with a deliberately altered verifier (e.g., append a character). A compliant server should return
invalid_grant.
Actionable takeaway
If you are building a public client (SPA, mobile, desktop) that uses the OAuth 2.0 authorization code flow, enable PKCE. Generate a fresh, high‑entropy verifier for each authorization request, derive the challenge with SHA‑256 and base64url‑without‑padding, and send the verifier only in the token request. Verify the challenge locally before sending the authorization request, and test that a modified verifier is rejected. This simple addition stops code‑injection attacks without requiring a client secret.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.