
What Is PKCE?
PKCE (Proof Key for Code Exchange) is a security extension to OAuth 2.0 that protects the Authorization Code flow from interception attacks—especially for public clients like mobile apps, SPAs, desktop apps, and CLI tools that can’t safely store a client secret.
At its core, PKCE binds the authorization request and the token request using a pair of values:
- code_verifier – a high-entropy, random string generated by the client.
- code_challenge – a transformed version of the verifier (usually SHA-256, base64url-encoded) sent on the initial authorization request.
Only the app that knows the original code_verifier can exchange the authorization code for tokens.
A Brief History
- 2015 — RFC 7636 formally introduced PKCE to mitigate “authorization code interception” attacks, first targeting native apps (mobile/desktop).
- 2017–2020 — Broad adoption across identity providers (IdPs) and SDKs made PKCE the de-facto choice for public clients.
- OAuth 2.1 (draft) consolidates best practices by recommending Authorization Code + PKCE (and deprecating the implicit flow) for browsers and mobile apps.
Bottom line: PKCE evolved from a “mobile-only hardening” to best practice for all OAuth clients, including SPAs.
How PKCE Works (Step-by-Step)
- App creates a code_verifier
- A cryptographically random string: 43–128 characters from
[A-Z] [a-z] [0-9] - . _ ~.
- A cryptographically random string: 43–128 characters from
- App derives a code_challenge
code_challenge = BASE64URL(SHA256(code_verifier))- Sets
code_challenge_method=S256(preferred).plainexists for legacy, but avoid it.
- User authorization request (front-channel)
- Browser navigates to the Authorization Server (AS)
/authorizewith:- response_type=code
- client_id=…
- redirect_uri=…
- scope=…
- state=…
- code_challenge=<derived value>
- code_challenge_method=S256
- Browser navigates to the Authorization Server (AS)
- User signs in & consents
- AS authenticates the user and redirects back to
redirect_uriwithcode(andstate).
- AS authenticates the user and redirects back to
- Token exchange (back-channel)
- App POSTs to
/tokenwith:grant_type=authorization_code code=<received code> redirect_uri=... client_id=... code_verifier=<original random string> - The AS recomputes
BASE64URL(SHA256(code_verifier))and compares to the storedcode_challenge.
- App POSTs to
- Tokens issued
- If the verifier matches, the AS returns tokens (access/refresh/ID token).
If an attacker steals the authorization code during step 4, it’s useless without the original code_verifier.
How PKCE Works (Step-by-Step)
- App creates a code_verifier
- A cryptographically random string: 43–128 characters from
[A-Z] [a-z] [0-9] - . _ ~.
- A cryptographically random string: 43–128 characters from
- App derives a code_challenge
code_challenge = BASE64URL(SHA256(code_verifier))- Sets
code_challenge_method=S256(preferred).plainexists for legacy, but avoid it.
- User authorization request (front-channel)
- Browser navigates to the Authorization Server (AS)
/authorizewith:
- Browser navigates to the Authorization Server (AS)
response_type=code
client_id=...
redirect_uri=...
scope=...
state=...
code_challenge=<derived value>
code_challenge_method=S256
- User signs in & consents
- AS authenticates the user and redirects back to
redirect_uriwithcode(andstate).
- AS authenticates the user and redirects back to
- Token exchange (back-channel)
- App POSTs to
/tokenwith:
- App POSTs to
grant_type=authorization_code
code=<received code>
redirect_uri=...
client_id=...
code_verifier=<original random string>
- Tokens issued
- If the verifier matches, the AS returns tokens (access/refresh/ID token).
If an attacker steals the authorization code during step 4, it’s useless without the original code_verifier.
Key Components & Features
- code_verifier: High-entropy, single-use secret generated per login attempt.
- code_challenge: Deterministic transform of the verifier; never sensitive on its own.
- S256 method: Strong default (
code_challenge_method=S256);plainonly for edge cases. - State & nonce: Still recommended for CSRF and replay protections alongside PKCE.
- Redirect URI discipline: Exact matching, HTTPS (for web), and claimed HTTPS URLs on mobile where possible.
- Back-channel token exchange: Reduces exposure compared to implicit flows.
Advantages & Benefits
- Mitigates code interception (custom URI handlers, OS-level handoff, browser extensions, proxies).
- No client secret required for public clients; still robust for confidential clients.
- Works everywhere (mobile, SPA, desktop, CLI).
- Backwards compatible with Authorization Code flow; easy to enable on most IdPs.
- Aligns with OAuth 2.1 best practices and most security recommendations.
Known Weaknesses & Limitations
- Not a phishing cure-all: PKCE doesn’t stop users from signing into a fake AS. Use trusted domains, phishing-resistant MFA, and App-Bound Domains on mobile.
- Verifier theft: If the code_verifier is leaked (e.g., via logs, devtools, or XSS in a SPA), the protection is reduced. Treat it as a secret at runtime.
- Still requires TLS and correct redirect URIs: Misconfigurations undermine PKCE.
- SPA storage model: In-browser JS apps must guard against XSS and avoid persisting sensitive artifacts unnecessarily.
Why You Should Use PKCE
- You’re building mobile, SPA, desktop, or CLI apps.
- Your security posture targets Authorization Code (not implicit).
- Your IdP supports it (almost all modern ones do).
- You want a future-proof, standards-aligned OAuth setup.
Use PKCE by default. There’s almost no downside and plenty of upside.
Integration Patterns (By Platform)
Browser-Based SPA
- Prefer Authorization Code + PKCE over implicit.
- Keep the code_verifier in memory (not localStorage) when possible.
- Use modern frameworks/SDKs that handle the PKCE dance.
Pseudo-JS example (client side):
// 1) Create code_verifier and code_challenge
const codeVerifier = base64UrlRandom(64);
const codeChallenge = await s256ToBase64Url(codeVerifier);
// 2) Start the authorization request
const params = new URLSearchParams({
response_type: 'code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'openid profile email',
state: cryptoRandomState(),
code_challenge: codeChallenge,
code_challenge_method: 'S256'
});
window.location.href = `${AUTHORIZATION_ENDPOINT}?${params.toString()}`;
// 3) On callback: exchange code for tokens (via your backend or a secure PKCE-capable SDK)
Tip: Many teams terminate the token exchange on a lightweight backend to reduce token handling in the browser and to set httpOnly, Secure cookies.
Native Mobile (iOS/Android)
- Use App/AS-supported SDKs with PKCE enabled.
- Prefer claimed HTTPS redirects (Apple/Android App Links) over custom schemes when possible.
Desktop / CLI
- Use the system browser with loopback (
http://127.0.0.1:<port>) redirect URIs and PKCE. - Ensure the token exchange runs locally and never logs secrets.
Server-Side Web Apps (Confidential Clients)
- You usually have a client secret—but add PKCE anyway for defense-in-depth.
- Many frameworks (Spring Security, ASP.NET Core, Django) enable PKCE with a toggle.
Provider & Framework Notes
- Spring Security: PKCE is on by default for public clients; for SPAs, combine with OAuth2 login and an API gateway/session strategy.
- ASP.NET Core: Set
UsePkce = trueon OpenIdConnect options. - Node.js: Use libraries like
openid-client,@azure/msal-browser,@okta/okta-auth-js, or provider SDKs with PKCE support. - IdPs: Auth0, Okta, Azure AD/Microsoft Entra, AWS Cognito, Google, Apple, and Keycloak all support PKCE.
(Exact flags differ per SDK; search your stack’s docs for “PKCE”.)
Rollout Checklist
- Enable PKCE on your OAuth client configuration (IdP).
- Use S256, never
plainunless absolutely forced. - Harden redirect URIs (HTTPS, exact match; mobile: app-bound/claimed links).
- Generate strong verifiers (43–128 chars; cryptographically random).
- Store verifier minimally (memory where possible; never log it).
- Keep state/nonce protections in place.
- Enforce TLS everywhere; disable insecure transports.
- Test negative cases (wrong verifier, missing method).
- Monitor for failed PKCE validations and unusual callback patterns.
Testing PKCE End-to-End
- Unit: generator for
code_verifierlength/charset; S256 transform correctness. - Integration: full redirect round-trip; token exchange with correct/incorrect verifier.
- Security: XSS scanning for SPAs; log review to confirm no secrets are printed.
- UX: deep links on mobile; fallback flows if no system browser available.
Common Pitfalls (and Fixes)
invalid_granton /token: Verifier doesn’t match challenge.- Recompute S256 and base64url without padding; ensure you used the same verifier used to create the challenge.
- Mismatched redirect_uri:
- The exact redirect URI in
/tokenmust match what was used in/authorize.
- The exact redirect URI in
- Leaky logs:
- Sanitize server and client logs; mask query params and token bodies.
Frequently Asked Questions
Do I still need a client secret?
- Public clients (mobile/SPA/CLI) can’t keep one—PKCE compensates. Confidential clients should keep the secret and may add PKCE.
Is PKCE enough for SPAs?
- It’s necessary but not sufficient. Also apply CSP, XSS protections, and consider backend-for-frontend patterns.
Why S256 over plain?
S256prevents trivial replay if the challenge is observed;plainoffers minimal value.
Conclusion
PKCE is a small change with huge security payoff. Add it to any Authorization Code flow—mobile, web, desktop, or CLI—to harden against code interception and align with modern OAuth guidance.
Recent Comments