If OAuth terms feel confusing, you are not alone.
This post explains Authorization Code Flow with PKCE in plain language with simple examples.
What Problem Does PKCE Solve?
In OAuth Authorization Code Flow, the app gets an authorization code first, then exchanges it for tokens.
The risk: if an attacker steals that authorization code, they may try to exchange it and get tokens.
PKCE (Proof Key for Code Exchange) adds a one-time secret so the stolen code alone is not enough.
In short:
- App creates a random secret called
code_verifier - App derives
code_challengefrom that verifier - App sends only
code_challengeduring login - Later, app must send original
code_verifierto get tokens - Auth server verifies both match
If they do not match, token exchange fails.
Key Terms (Quick)
Authorization Endpoint: where user signs in and grants consentToken Endpoint: where app exchanges code for tokenscode_verifier: high-entropy random string created by clientcode_challenge: transformed version ofcode_verifier(usually SHA-256 + Base64URL)S256: recommended challenge method
Flow in 7 Steps
- Client app generates
code_verifier - Client app creates
code_challenge = BASE64URL(SHA256(code_verifier)) - Client redirects browser to authorization endpoint with
code_challengeandcode_challenge_method=S256 - User logs in and approves
- Authorization server redirects back with
authorization_code - Client sends
authorization_code+ originalcode_verifierto token endpoint - Server validates verifier/challenge match and returns tokens
Visual Sequence
Client App Authorization Server
---------- --------------------
Generate code_verifier
Create code_challenge
Browser -> /authorize?code_challenge=...&method=S256
User login + consent
<-- Redirect with authorization_code
POST /token
grant_type=authorization_code
code=...
code_verifier=... Validate verifier against challenge
<-- access_token (+ optional refresh_token, id_token)
Example 1: Build PKCE Values (JavaScript)
This example shows the core PKCE generation logic.
async function generatePkcePair() {
const randomBytes = new Uint8Array(32);
crypto.getRandomValues(randomBytes);
const codeVerifier = base64UrlEncode(randomBytes);
const data = new TextEncoder().encode(codeVerifier);
const digest = await crypto.subtle.digest("SHA-256", data);
const codeChallenge = base64UrlEncode(new Uint8Array(digest));
return { codeVerifier, codeChallenge };
}
function base64UrlEncode(bytes) {
let binary = "";
bytes.forEach((b) => (binary += String.fromCharCode(b)));
return btoa(binary)
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/g, "");
}
Example 2: Build PKCE Values (C#)
This C# example generates a secure code_verifier and code_challenge using S256.
using System;
using System.Security.Cryptography;
using System.Text;
public static class PkceGenerator
{
public static (string CodeVerifier, string CodeChallenge) Generate()
{
var randomBytes = new byte[32];
RandomNumberGenerator.Fill(randomBytes);
var codeVerifier = Base64UrlEncode(randomBytes);
var challengeBytes = SHA256.HashData(Encoding.ASCII.GetBytes(codeVerifier));
var codeChallenge = Base64UrlEncode(challengeBytes);
return (codeVerifier, codeChallenge);
}
private static string Base64UrlEncode(byte[] bytes)
{
return Convert.ToBase64String(bytes)
.TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
}
Example 3: Authorization Request
GET https://login.example.com/oauth2/v2.0/authorize?
response_type=code
&client_id=YOUR_CLIENT_ID
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&scope=openid%20profile%20api.read
&state=abc123
&code_challenge=YOUR_CODE_CHALLENGE
&code_challenge_method=S256
Notes:
statehelps prevent CSRF- Use HTTPS redirect URIs
- Keep scopes minimal
Example 4: Token Exchange Request
POST https://login.example.com/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&client_id=YOUR_CLIENT_ID
&code=AUTHORIZATION_CODE
&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcallback
&code_verifier=ORIGINAL_CODE_VERIFIER
If code_verifier does not correspond to the original challenge, token issuance is denied.
Where PKCE Is Required (and Why)
PKCE is strongly recommended for all OAuth clients and essential for public clients:
- Single-page apps (SPA)
- Mobile apps
- Desktop apps
These clients cannot safely store a client secret, so PKCE is the security anchor for code exchange.
Common Mistakes
-
Using
plaininstead ofS256UseS256unless there is a hard compatibility reason. -
Reusing the same verifier Create a new
code_verifierfor every authorization request. -
Missing state validation Always validate
stateon callback. -
Weak verifier randomness Use cryptographically secure random bytes, not
Math.random(). -
Forgetting redirect URI consistency The
redirect_uriat token exchange must match the one used in authorization.
Final Thoughts
Authorization Code Flow with PKCE is the modern default for secure OAuth login in browser and mobile applications.
If you remember just one thing, remember this:
PKCE binds the authorization code to the original client request, making stolen codes much less useful.
References