Securing Public Clients: Why PKCE is the New Standard for OAuth 2.0
Public clients can't keep secrets. Learn how PKCE (Proof Key for Code Exchange) secures OAuth 2.0 flows for SPAs and mobile apps by replacing static secrets with dynamic, one-time verifiers.
28 Aug 2026, 05:10 UTC

The Problem: The 'Secret' That Isn't Secret
In the original OAuth 2.0 Authorization Code flow, a client application exchanges an authorization code for an access token using a client_secret. This works for server-side apps where the secret stays hidden on a secure server. However, for "public clients"—like Single Page Applications (SPAs) or mobile apps—there is no secure place to store a secret. If you embed a client_secret in JavaScript or a compiled binary, any user can find it using browser developer tools or a decompiler.
Without a secret, a malicious actor who intercepts the authorization code (via a custom URI scheme or browser history) could potentially exchange that code for a token before the legitimate app does. This is where PKCE (Proof Key for Code Exchange) becomes essential. The takeaway is simple: never rely on a static client secret for public clients; use PKCE to create a dynamic, one-time secret for every single request.
How PKCE Works: The Dynamic Handshake
PKCE (RFC 7636) replaces the static secret with a cryptographically linked pair of values generated on the fly for every authorization attempt.
- Code Verifier: A high-entropy random string (43–128 characters) generated by the client. This is the "secret" for this specific session.
- Code Challenge: A transformed version of the verifier. Usually, the client hashes the verifier using SHA-256 and then Base64URL encodes it.
The flow changes as follows: the client sends the code_challenge during the initial authorization request. The server stores this challenge. When the client later exchanges the authorization code for a token, it sends the original code_verifier. The server hashes the verifier; if the result matches the stored challenge, it proves the client requesting the token is the same one that started the flow.
Implementation Guide and Configuration
When implementing PKCE, always use the S256 challenge method. The plain method exists for legacy devices that cannot perform SHA-256, but it provides significantly less protection against interception.
Example: Constructing the Request
Assuming a client is running in a browser or mobile environment, the sequence looks like this:
1. Generate the Verifier (Client-side)
// Example logic: Generate a random string of 64 characters
const code_verifier = generateRandomString(64);
2. Create the Challenge (Client-side)
// Hash the verifier with SHA-256, then Base64URL encode
const code_challenge = base64UrlEncode(sha256(code_verifier));
3. The Authorization Request (Browser Redirect)
Run this as a GET request to the Authorization Server's /authorize endpoint:
GET /authorize?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https://app.example.com/callback
&scope=openid profile
&state=RANDOM_CSRF_TOKEN
&code_challenge=THE_GENERATED_CHALLENGE
&code_challenge_method=S256
4. The Token Exchange (POST Request)
Once the server redirects back with a code, the client makes a POST request to the /token endpoint using its own credentials (usually just the client_id for public clients):
POST /token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&code=RECEIVED_AUTH_CODE
&redirect_uri=https://app.example.com/callback
&code_verifier=THE_ORIGINAL_VERIFIER
Critical Trade-offs and Limitations
While PKCE solves the "secret" problem, it is not a silver bullet for all security concerns:
- CSRF Protection: PKCE does not replace the
stateparameter. While PKCE ensures the same client finishes the flow, thestateparameter ensures the user's browser session wasn't hijacked to start the flow. Use both. - Token Storage: PKCE protects the exchange of the code for a token. It does not protect the token once it is in the browser. Storing tokens in
localStoragestill leaves them vulnerable to Cross-Site Scripting (XSS). For high-security SPAs, consider the Backend-for-Frontend (BFF) pattern to keep tokens in secure, HTTP-only cookies. - Computational Overhead: The SHA-256 hashing is negligible for modern devices, but the client must ensure the
code_verifieris stored in memory or session storage and cleared immediately after the exchange.
Verifying Your Implementation
To verify that PKCE is working correctly, you can perform a "tamper test" using a proxy tool or browser developer tools:
- Start the authorization flow and capture the
codereturned to your redirect URI. - Intercept the POST request to the
/tokenendpoint. - Change a single character in the
code_verifiervalue. - Expected Result: The Authorization Server must reject the request with an
invalid_granterror. If the server issues a token despite the modified verifier, PKCE is not being enforced, and your client remains vulnerable.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.