~/blog

oauth2-pkce-spa-security.md

OAuth2 + PKCE: Auth for Modern Single-Page Apps

Why implicit flow died, how PKCE protects public clients, and the token flow every React app should implement.

April 28, 20252 min read
  • Security
  • OAuth
  • Web
OAuth2 + PKCE: Auth for Modern Single-Page Apps

SPAs can't keep secrets โ€” your client ID lives in bundled JavaScript. OAuth2's implicit flow treated that as acceptable. It wasn't. PKCE fixed public client auth without a backend holding credentials.

The actors

  • Resource owner โ€” the user
  • Client โ€” your SPA or mobile app
  • Authorization server โ€” issues tokens (Auth0, Keycloak)
  • Resource server โ€” your API

Authorization code + PKCE flow

  1. Generate random code_verifier (43โ€“128 chars)
  2. Hash it โ†’ code_challenge (S256)
  3. Redirect user to auth server with challenge
  4. User logs in, auth server returns authorization code
  5. Exchange code + original verifier for tokens
// Step 1 โ€” before redirect
const verifier = generateRandomString(64);
const challenge = base64url(sha256(verifier));
sessionStorage.setItem("pkce_verifier", verifier);
 
// Step 5 โ€” token exchange
const res = await fetch("/oauth/token", {
  method: "POST",
  body: new URLSearchParams({
    grant_type: "authorization_code",
    code,
    code_verifier: sessionStorage.getItem("pkce_verifier"),
    client_id: CLIENT_ID,
    redirect_uri: REDIRECT_URI,
  }),
});

An attacker intercepting the code can't exchange it without the verifier they never saw.

Token storage

Storage XSS risk Refresh
localStorage High Easy
memory Lower Lost on refresh
httpOnly cookie Lowest Needs BFF pattern

For SPAs, Backend-for-Frontend (BFF) holding refresh tokens in httpOnly cookies is the production-grade pattern.

What to avoid

  • Implicit flow โ€” deprecated (RFC 9700)
  • Long-lived access tokens in localStorage
  • Client secrets in frontend bundles

[!WARNING] OAuth2 is a framework, not a plug-and-play library. Misconfigured redirect URIs and scopes cause more breaches than crypto breaks.

Takeaway

PKCE turns "public client" from a vulnerability into a defined threat model. Pair it with short-lived access tokens and secure refresh handling โ€” your users (and compliance team) will thank you.

Related

Continue reading

More notes on similar topics.

A living reference for writing posts โ€” thumbnails, code, tables, alerts, footnotes, images, audio, video, and YouTube embeds.

  • Markdown
  • Blog
4 min