PKCE for Single-Page Apps
OAuth tutorials from 2018 still show response_type=token and client_secret in React env vars. Both are wrong for browser apps. PKCE (Proof Key for Code Exchange) turns the authorization code into a one-time pairing: your SPA generates a random verifier, sends a hash challenge to the authorize endpoint, then proves possession when exchanging the code. Attackers who sniff the redirect ?code= cannot redeem it without the verifier that never left the session.
Generate verifier and challenge
function randomVerifier(): string {
const array = new Uint8Array(32);
crypto.getRandomValues(array);
return base64UrlEncode(array);
}
async function challengeFromVerifier(verifier: string): Promise<string> {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest("SHA-256", data);
return base64UrlEncode(new Uint8Array(digest));
}
Store code_verifier in sessionStorage for the auth round trip—cleared after exchange. Use S256 method; plain is for constrained clients only.
GET https://auth.example.com/oauth/authorize?
response_type=code
&client_id=spa_client_id
&redirect_uri=https://app.example.com/callback
&scope=openid profile
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
&state=CsrfRandomState
Validate state on callback against sessionStorage to block CSRF.
const res = await fetch("https://auth.example.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
client_id: SPA_CLIENT_ID,
code: authorizationCode,
redirect_uri: REDIRECT_URI,
code_verifier: sessionStorage.getItem("code_verifier")!,
}),
});
No client_secret in browser. Token endpoint must require matching verifier.
Browser → BFF (/session/login) → IdP
IdP → BFF (/callback) sets HttpOnly cookie
Browser → BFF (/api/*) with cookie
BFF attaches access token server-side
Refresh tokens never touch JavaScript. BFF validates CSRF on mutating routes.
Register exact URIs—no wildcards in production. Reject open redirects chained off callback parameters.
PKCE stops code interception; XSS still steals tokens. Enforce strict CSP, sanitize HTML, and keep access tokens short-lived (5–15 minutes).
CSP and XSS remain risk after PKCE—short-lived access tokens limit exposure. BFF pattern keeps refresh tokens in HttpOnly cookies never touched by JavaScript.
Redirect URI discipline: exact matches in production, no open redirect chains off callback parameters. Register separate clients for web and mobile rather than sharing redirect URIs loosely.
Document refresh failure UX—silent re-login versus explicit session expired messaging affects support volume and security perception.
Public clients cannot hold secrets
SPAs are public OAuth clients—any embedded secret is extractable from bundle. PKCE replaces client secret with code_verifier/code_challenge pair bound to authorization request.
const verifier = generateRandomString(64);
const challenge = await sha256Base64Url(verifier);
// store verifier in sessionStorage for token exchange
Authorization code interception
Without PKCE, malicious app registers same custom URL scheme and intercepts auth code on mobile. PKCE ensures token endpoint rejects exchange without verifier.
Refresh token rotation
SPAs using refresh tokens need rotation detection—reuse of old refresh token revokes entire token family. Store refresh token in HttpOnly cookie via backend-for-frontend pattern when possible.
Sec Oauth Pkce Spa: operational depth
PKCE is mandatory for public OAuth clients—any secret in your SPA bundle is public. Teams that skip instrumentation ship blind—baseline p75 latency and error rate on affected routes one week before change and compare seven days after.
Integration boundaries deserve contract tests with golden fixtures sampled from production traffic anonymized. Synthetic empty payloads pass CI while production fails on nullable fields you never modeled.
Security review asks three questions: what untrusted input enters, what secrets could leak in logs, and what happens when upstream is slow or malicious. Answers belong in the PR, not a post-launch wiki page.
Rollout prefers feature flags or canary deploys when behavior touches authentication, payments, or PII. Rollback command documented in runbook header—not discovered during incident via git archaeology.
On-call dashboards slice metrics by region and device class. Global averages hide mobile regressions until App Store reviews mention slowness—field data honesty beats demo Lighthouse scores.
Resources
- RFC 7636: PKCE
- OAuth 2.0 for Browser-Based Apps (BCP)
- OAuth 2.1 draft
- Auth0 PKCE guide
- OWASP SPA security cheat sheet
Extended guidance (1) for Sec Oauth Pkce Spa
Operators owning sec oauth pkce spa should run a pre-mortem before launch: dependency unavailable, duplicate events, certificate expiry, regional failover. Each scenario needs detectable metrics, a runbook step, and a tested rollback. Game days beat postmortems for building muscle memory.
Contract tests at boundaries use anonymized production samples—nullable fields and unicode edge cases break synthetic fixtures. Security review documents untrusted inputs and log redaction rules in the PR description so auditors and on-call engineers inherit context without archaeology.
Performance work ties to field data on mid-tier mobile hardware, not desktop lab profiles. Slice dashboards by route, deploy version, and region before declaring victory on global averages.
Extended guidance (2) for Sec Oauth Pkce Spa
Operators owning sec oauth pkce spa should run a pre-mortem before launch: dependency unavailable, duplicate events, certificate expiry, regional failover. Each scenario needs detectable metrics, a runbook step, and a tested rollback. Game days beat postmortems for building muscle memory.
Contract tests at boundaries use anonymized production samples—nullable fields and unicode edge cases break synthetic fixtures. Security review documents untrusted inputs and log redaction rules in the PR description so auditors and on-call engineers inherit context without archaeology.
Performance work ties to field data on mid-tier mobile hardware, not desktop lab profiles. Slice dashboards by route, deploy version, and region before declaring victory on global averages.
Extended guidance (3) for Sec Oauth Pkce Spa
Operators owning sec oauth pkce spa should run a pre-mortem before launch: dependency unavailable, duplicate events, certificate expiry, regional failover. Each scenario needs detectable metrics, a runbook step, and a tested rollback. Game days beat postmortems for building muscle memory.
Contract tests at boundaries use anonymized production samples—nullable fields and unicode edge cases break synthetic fixtures. Security review documents untrusted inputs and log redaction rules in the PR description so auditors and on-call engineers inherit context without archaeology.
Performance work ties to field data on mid-tier mobile hardware, not desktop lab profiles. Slice dashboards by route, deploy version, and region before declaring victory on global averages.
Extended guidance (4) for Sec Oauth Pkce Spa
Operators owning sec oauth pkce spa should run a pre-mortem before launch: dependency unavailable, duplicate events, certificate expiry, regional failover. Each scenario needs detectable metrics, a runbook step, and a tested rollback. Game days beat postmortems for building muscle memory.
Contract tests at boundaries use anonymized production samples—nullable fields and unicode edge cases break synthetic fixtures. Security review documents untrusted inputs and log redaction rules in the PR description so auditors and on-call engineers inherit context without archaeology.
Performance work ties to field data on mid-tier mobile hardware, not desktop lab profiles. Slice dashboards by route, deploy version, and region before declaring victory on global averages.
Extended guidance (5) for Sec Oauth Pkce Spa
Operators owning sec oauth pkce spa should run a pre-mortem before launch: dependency unavailable, duplicate events, certificate expiry, regional failover. Each scenario needs detectable metrics, a runbook step, and a tested rollback. Game days beat postmortems for building muscle memory.
Contract tests at boundaries use anonymized production samples—nullable fields and unicode edge cases break synthetic fixtures. Security review documents untrusted inputs and log redaction rules in the PR description so auditors and on-call engineers inherit context without archaeology.
Performance work ties to field data on mid-tier mobile hardware, not desktop lab profiles. Slice dashboards by route, deploy version, and region before declaring victory on global averages.
Frequently asked questions
Why is PKCE required for SPAs?
SPAs cannot hold client secrets—JavaScript bundles are public. Without PKCE, an attacker who intercepts the authorization code can exchange it at the token endpoint. PKCE binds the code to a verifier generated by the legitimate app instance, so stolen codes are useless without the verifier stored in that browser session.
Should SPAs store refresh tokens in localStorage?
Avoid localStorage for refresh tokens—they are readable by any XSS vulnerability. Prefer HttpOnly Secure SameSite cookies set by a backend-for-frontend or use refresh token rotation with strict Content Security Policy and short access token TTL if pure front-channel is unavoidable.
Can I use implicit flow instead?
No. Implicit flow returns access tokens in the URL fragment and is deprecated in OAuth 2.1. Authorization Code with PKCE is the standard for browser apps.
Hiring a senior Android / Flutter engineer?
I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.
Get in touch →