Blog
3 min read

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:

  1. Your app sends the user to the provider (Google, GitHub) to log in and approve access.
  2. The provider redirects back to your app with a short-lived authorization code in the URL.
  3. 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; use S256.
  • 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 — localStorage is 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