PKCE for Public OAuth Clients: Block Authorization Code Interception
PKCE binds the OAuth 2.0 authorization code flow to a client-generated verifier so public clients can redeem codes safely. Includes a Bash example, server validation steps, and limitations.
15 Jul 2026, 15:50 UTC

Problem: Public clients cannot protect a client secret
When a mobile app or single-page application (SPA) starts the OAuth 2.0 authorization code flow, it sends the user to an authorization server and expects an authorization code in return. The app then exchanges that code at a token endpoint for an access token. Public clients cannot store a client secret, so the authorization code is the only thing standing between an attacker and a valid access token. If a malicious app registers the same redirect URI, or if a user is tricked through a compromised browser, an intercepted code can be exchanged by anyone who possesses it.
PKCE (Proof Key for Code Exchange) addresses this by binding the authorization request to the token exchange. The client creates a cryptographically random value called a code_verifier, hashes it to produce a code_challenge, and sends the challenge in the authorization request. Later, the client sends the original verifier to the token endpoint. The server hashes the verifier and compares it to the stored challenge. If they do not match, the exchange fails.
Important correction: the verifier is not sent in the authorization request, but it is transmitted to the token endpoint during the exchange. It is the missing link that proves the same client started and finished the flow.
Worked example: generate and use PKCE in Bash
Run the following on the client machine or build host. No special permissions are required. Replace the placeholder values with your own client ID, redirect URI, and authorization server URLs.
# Configuration
AUTH_URL="https://auth.example.com/authorize"
TOKEN_URL="https://auth.example.com/token"
CLIENT_ID="my-public-client"
REDIRECT_URI="https://app.example.com/callback"
SCOPE="openid profile"
# 1. Generate a high-entropy code verifier (RFC 7636 recommends 43-128 characters)
code_verifier=$(openssl rand -base64 32 | tr -d '=+/' | cut -c1-128)
# 2. Derive the S256 code challenge: SHA-256 of the verifier, URL-safe base64 without padding
code_challenge=$(printf '%s' "$code_verifier" | openssl dgst -sha256 -binary | openssl base64 -e -A | tr '+/' '-_' | tr -d '=')
# 3. URL-encode query components before assembling the authorization URL
encoded_redirect=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$REDIRECT_URI")
encoded_scope=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$SCOPE")
encoded_challenge=$(python3 -c 'import sys,urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' "$code_challenge")
AUTH_REQUEST="${AUTH_URL}?response_type=code&client_id=${CLIENT_ID}&redirect_uri=${encoded_redirect}&scope=${encoded_scope}&code_challenge=${encoded_challenge}&code_challenge_method=S256"
echo "Open this URL in a browser or web view:"
echo "$AUTH_REQUEST"
After the user approves the request, the authorization server redirects back to your redirect_uri with an authorization code in the query string. Capture that code, then exchange it:
AUTHORIZATION_CODE="CODE_FROM_REDIRECT"
curl -X POST "$TOKEN_URL" \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=authorization_code" \
--data-urlencode "code=${AUTHORIZATION_CODE}" \
--data-urlencode "redirect_uri=${REDIRECT_URI}" \
--data-urlencode "client_id=${CLIENT_ID}" \
--data-urlencode "code_verifier=${code_verifier}"
The code_verifier is sent only in this token request. Keep it in memory and discard it after the exchange. If the server returns an error such as invalid_grant, the verifier likely did not match the stored challenge.
Library integration and server validation
Most OAuth client libraries can generate the verifier and challenge automatically, but behavior varies by language and library version. Do not assume PKCE is enabled by default; check your library's documentation for the authorization code flow. When configuring the authorization server, confirm it accepts the S256 method. The plain method exists for legacy systems but offers no meaningful protection because the challenge is the verifier itself.
Before relying on PKCE in production, confirm that the server enforces it. A server that accepts a token request without code_verifier when one was expected is not enforcing the binding.
Trade-offs and limitations
- Entropy matters. Use a cryptographically secure random source. A weak or predictable verifier defeats the purpose of PKCE.
- Storage risk. Do not persist the verifier in local storage, cookies, or application logs. Keep it in memory only until the token exchange completes.
- Not a complete defense. PKCE does not protect against a fully compromised device, a malicious extension, or a phishing site that captures credentials before the redirect.
- Confidential clients. For server-side applications that can store a client secret, PKCE is an additional layer, not a replacement for client authentication.
Practical verification
- Run the full flow with a correct
code_verifier. The token endpoint should return an access token. - Repeat the exchange with an incorrect or omitted
code_verifier. The server should reject the request, typically with HTTP 400 and aninvalid_granterror. - Inspect the authorization server's metadata or documentation to confirm it supports
code_challenge_method=S256.
Actionable takeaways
- Enable PKCE for every public client: mobile apps, desktop apps, and SPAs.
- Use
S256, notplain, unless the server lacks support. - Generate the verifier with a secure random source and hold it in memory only.
- URL-encode all authorization-request parameters, including the challenge and redirect URI.
- Test that your authorization server rejects missing or mismatched verifiers before deploying.
PKCE turns the authorization code from a bearer-like token into a value that only the originating client can redeem. It is not a substitute for good session and device security, but it is the baseline protection public clients need.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.