OAuth PKCE Explained: Why Every OAuth Flow Should Use It
PKCE (Proof Key for Code Exchange) protects the OAuth authorization code flow from code interception. How code_verifier and code_challenge work step by step, why it's required for SPAs and mobile apps and recommended for everyone, the state parameter, and the mistakes to avoid.
When an app lets you "Sign in with Google" or "Connect your GitHub account", it's using OAuth 2.0. The modern, recommended flow is the authorization code flow with PKCE (pronounced "pixy"). (Sign in with Google explained)
The problem PKCE solves
In the authorization code flow:
- Your app sends the user to the provider (Google, GitHub) to log in and approve access.
- The provider redirects back to your app with a short-lived authorization code in the URL.
- Your app exchanges that code for tokens.
The weak point is step 2: the code travels through the browser. On mobile, another app can register the same custom URL scheme and receive it; in browsers, codes can leak through logs, history or malicious extensions. Whoever has the code could exchange it for tokens.
Traditional server apps protect step 3 with a client secret only the server knows. But single-page apps and mobile apps can't keep a secret — anything in them can be extracted. (Keep API keys out of an AI-built app)
PKCE makes a stolen code useless.
How PKCE works
Before redirecting, your app creates a one-time secret and sends only a hash of it:
// 1. random secret (43–128 chars)
const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(32)))
// 2. its SHA-256 hash
const codeChallenge = base64url(
new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier)))
)
// store codeVerifier for later (session storage / server session)
Step 1 — authorise, sending the challenge:
https://provider.example/authorize?
response_type=code
&client_id=abc123
&redirect_uri=https://app.example/callback
&scope=openid email
&state=xyz789
&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
&code_challenge_method=S256
Step 2 — callback with ?code=...&state=xyz789.
Step 3 — exchange, sending the original verifier:
POST /token
grant_type=authorization_code
&code=...
&redirect_uri=https://app.example/callback
&client_id=abc123
&code_verifier=<the original random secret>
The provider hashes the verifier and checks it matches the challenge from step 1. An attacker who intercepted the code doesn't have the verifier — it never left your app — so the exchange fails.
Who should use PKCE
- SPAs and mobile/desktop apps (public clients): required.
- Server-side apps with a client secret: recommended too. The OAuth 2.1 draft and current security best practice make PKCE the default for all authorization code flows, because it also defends against code injection attacks.
The old implicit flow (tokens directly in the URL) is deprecated — use code + PKCE instead.
Don't forget state
The state parameter is a random value you generate, store, and check on the callback. It protects against CSRF — an attacker tricking a user's browser into completing the attacker's login. PKCE helps here too, but use both. (CSRF explained)
Common mistakes
code_challenge_method=plain— sends the verifier unhashed; useS256.- Reusing verifiers — generate a fresh one for every login.
- Loose redirect URIs — register exact URLs; wildcards enable token theft. (Open redirect vulnerability)
- Storing tokens carelessly in SPAs —
localStorageis readable by any XSS. Many apps use a backend-for-frontend that keeps tokens server-side and gives the browser an HttpOnly session cookie. (localStorage vs cookies, XSS explained) - Implementing it by hand in production — use a maintained library or your auth provider's SDK. (Clerk vs Auth0 vs Supabase Auth)
PKCE in the wild
You've likely used it without knowing: auth libraries (Auth.js, Better Auth, Supabase, Clerk) use it for social logins, and MCP's authorization specification requires OAuth 2.1 with PKCE when AI clients connect to remote MCP servers. (What is MCP?)
After the exchange
You receive an access token (short-lived) and often a refresh token to get new access tokens. (Refresh tokens explained, What is a JWT?)
EasySpawn serves your app over HTTPS on a stable domain with a real backend, so OAuth redirect URIs are exact and tokens can stay on the server where they belong. See how it works or join the waitlist.
Related: "Sign in with Google" Explained · Refresh Tokens Explained · API Authentication Methods · Session Cookies vs JWTs
Keep reading
Refresh Tokens Explained: Short-Lived Access, Long-Lived Sessions
Access tokens should expire quickly; refresh tokens let users stay logged in without re-entering passwords. How the pair works, where to store each, refresh token rotation and reuse detection, revocation, handling refresh in the front end, and when plain session cookies are simpler.
Next.js Server Actions: Forms and Mutations Without API Routes
Server Actions (Server Functions) let a form or button call a function that runs on the server, without writing an API route. How 'use server' works, forms with useActionState, validation with Zod, revalidating data, pending states — and why every action is a public endpoint that needs its own auth checks.