What is PKCE?

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)

  1. App creates a code_verifier
    • A cryptographically random string: 43–128 characters from [A-Z] [a-z] [0-9] - . _ ~.
  2. App derives a code_challenge
    • code_challenge = BASE64URL(SHA256(code_verifier))
    • Sets code_challenge_method=S256 (preferred). plain exists for legacy, but avoid it.
  3. User authorization request (front-channel)
    • Browser navigates to the Authorization Server (AS) /authorize with:
      • response_type=code
      • client_id=…
      • redirect_uri=…
      • scope=…
      • state=…
      • code_challenge=<derived value>
      • code_challenge_method=S256
  4. User signs in & consents
    • AS authenticates the user and redirects back to redirect_uri with code (and state).
  5. Token exchange (back-channel)
    • App POSTs to /token with: 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 stored code_challenge.
  6. 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)

  1. App creates a code_verifier
    • A cryptographically random string: 43–128 characters from [A-Z] [a-z] [0-9] - . _ ~.
  2. App derives a code_challenge
    • code_challenge = BASE64URL(SHA256(code_verifier))
    • Sets code_challenge_method=S256 (preferred). plain exists for legacy, but avoid it.
  3. User authorization request (front-channel)
    • Browser navigates to the Authorization Server (AS) /authorize with:
response_type=code
client_id=...
redirect_uri=...
scope=...
state=...
code_challenge=<derived value>
code_challenge_method=S256

  1. User signs in & consents
    • AS authenticates the user and redirects back to redirect_uri with code (and state).
  2. Token exchange (back-channel)
    • App POSTs to /token with:
grant_type=authorization_code
code=<received code>
redirect_uri=...
client_id=...
code_verifier=<original random string>

  1. 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); plain only 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 = true on 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

  1. Enable PKCE on your OAuth client configuration (IdP).
  2. Use S256, never plain unless absolutely forced.
  3. Harden redirect URIs (HTTPS, exact match; mobile: app-bound/claimed links).
  4. Generate strong verifiers (43–128 chars; cryptographically random).
  5. Store verifier minimally (memory where possible; never log it).
  6. Keep state/nonce protections in place.
  7. Enforce TLS everywhere; disable insecure transports.
  8. Test negative cases (wrong verifier, missing method).
  9. Monitor for failed PKCE validations and unusual callback patterns.

Testing PKCE End-to-End

  • Unit: generator for code_verifier length/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_grant on /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 /token must match what was used in /authorize.
  • 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?

  • S256 prevents trivial replay if the challenge is observed; plain offers 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.