PKCE OAuth Flow Testing: A Step-by-Step Guide for Frontend Developers
If you're building a single-page application or a mobile app, PKCE (Proof Key for Code Exchange) is the OAuth flow you should be using. It's recommended by the OAuth 2.0 Security Best Current Practice (RFC 9700) and is the default in most modern auth libraries.
But testing it is another story. PKCE adds cryptographic verification on top of the authorization code flow, and most mock servers don't implement it correctly — or at all.
What Is PKCE and Why Does It Matter?
PKCE (pronounced "pixy") prevents authorization code interception attacks. Without PKCE, anyone who intercepts the authorization code can exchange it for tokens. PKCE adds a secret (the code verifier) that only the original client knows.
The flow works like this:
- The client generates a random
code_verifier(43-128 character string). - The client computes
code_challenge = BASE64URL(SHA256(code_verifier)). - The client sends the
code_challengewith the authorization request. - The server stores the challenge alongside the authorization code.
- When the client exchanges the code for tokens, it sends the original
code_verifier. - The server computes SHA256(code_verifier) and compares it to the stored challenge.
- If they match, tokens are issued. If not, the request is rejected.
Why Testing PKCE Is Hard
Most testing setups skip PKCE because:
- Mock servers don't support it — simple OAuth mocks accept any code without PKCE verification.
- The cryptography is confusing — generating the code challenge correctly requires SHA-256 + base64url encoding.
- Auth libraries abstract it away — next-auth, oidc-client-ts, and AppAuth handle PKCE internally, so developers never see the raw flow.
- Testing the unhappy path is impossible — how do you test what happens when the code_verifier is wrong? A real IdP won't let you submit a bad verifier easily.
Step-by-Step PKCE Testing with Auth Sandbox
moqapi.dev's Auth Sandbox implements full PKCE verification (S256 method). Here's the complete flow:
Step 1: Generate the PKCE Pair
// Generate code_verifier and code_challenge
const crypto = require('crypto');
// code_verifier: 43-128 unreserved characters
const codeVerifier = crypto.randomBytes(32).toString('base64url');
// code_challenge: SHA-256 hash of verifier, base64url-encoded
const codeChallenge = crypto
.createHash('sha256')
.update(codeVerifier)
.digest('base64url');
console.log('Verifier:', codeVerifier); // e.g., "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
console.log('Challenge:', codeChallenge); // e.g., "E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"
Step 2: Start the Authorization Request
# Send the user to /authorize with the code_challenge
GET https://moqapi.dev/api/invoke/{projectId}/auth/authorize?
response_type=code&
client_id=moqapi_abc123&
redirect_uri=http://localhost:3000/callback&
scope=openid profile email&
state=csrf-protection-token&
code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&
code_challenge_method=S256
# The sandbox auto-approves and redirects:
→ http://localhost:3000/callback?code=sandbox_code_abc123&state=csrf-protection-token
Step 3: Exchange Code + Verifier for Tokens
curl -X POST https://moqapi.dev/api/invoke/{projectId}/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "sandbox_code_abc123",
"redirect_uri": "http://localhost:3000/callback",
"client_id": "moqapi_abc123",
"code_verifier": "dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk"
}'
# Response:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "sandbox_refresh_xyz789",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"scope": "openid profile email"
}
Step 4: Test the Wrong Verifier (Unhappy Path)
# Send a wrong code_verifier
curl -X POST https://moqapi.dev/api/invoke/{projectId}/auth/token \
-H "Content-Type: application/json" \
-d '{
"grant_type": "authorization_code",
"code": "sandbox_code_abc123",
"redirect_uri": "http://localhost:3000/callback",
"client_id": "moqapi_abc123",
"code_verifier": "this-is-not-the-right-verifier"
}'
# Response: 400
{
"error": "invalid_grant",
"error_description": "PKCE verification failed"
}
This is the critical test that most mock servers can't do. The sandbox actually computes SHA256 of the provided verifier and compares it to the stored challenge.
Testing PKCE with Auth Libraries
Most auth libraries handle PKCE automatically. Here's how to configure them to use the sandbox:
oidc-client-ts (for SPAs)
import { UserManager } from 'oidc-client-ts';
const config = {
authority: 'https://moqapi.dev/api/invoke/{projectId}/auth',
client_id: 'moqapi_abc123',
redirect_uri: 'http://localhost:3000/callback',
scope: 'openid profile email',
// PKCE is enabled by default in oidc-client-ts
};
const userManager = new UserManager(config);
// This triggers the full PKCE flow automatically
await userManager.signinRedirect();
AppAuth (for Mobile Apps)
// React Native with react-native-app-auth
import { authorize } from 'react-native-app-auth';
const config = {
issuer: 'https://moqapi.dev/api/invoke/{projectId}/auth',
clientId: 'moqapi_abc123',
redirectUrl: 'myapp://callback',
scopes: ['openid', 'profile', 'email'],
usePKCE: true, // enabled by default
};
const result = await authorize(config);
Common PKCE Testing Mistakes
- Using plain method instead of S256 — the
plaincode challenge method sends the verifier as-is, defeating the purpose. Always useS256. - Not testing verifier mismatch — your frontend should handle the
invalid_granterror when the verifier is wrong. The sandbox lets you test this. - Reusing authorization codes — each code is single-use. The sandbox correctly rejects codes that have already been exchanged.
- Skipping the state parameter — while not required by PKCE, the
stateparameter prevents CSRF attacks. Always include it.
Key Takeaways
- PKCE is the recommended OAuth flow for public clients (SPAs, mobile apps, CLIs).
- Testing PKCE requires an authorization server that correctly verifies SHA-256 code challenges.
- The Auth Sandbox implements full PKCE verification including unhappy path testing.
- Standard auth libraries (oidc-client-ts, AppAuth) work out of the box via OIDC discovery.
- Test both the happy path (correct verifier) and the unhappy path (wrong verifier, reused code).
Test PKCE flows without infrastructure at moqapi.dev/signup.
About the Author
Founder and sole developer of moqapi.dev. Full-stack engineer with deep experience in API platforms, serverless runtimes, and developer tooling. Built moqapi to solve the mock data and deployment friction she experienced firsthand building production APIs.
Related Articles
How to Build a Full Frontend Without a Real Backend Using Mock APIs
Your backend isn't ready — but the sprint deadline is. Here's the exact workflow for building production-quality UI with mock endpoints and no compromise on realism.
I Shipped a Full Dashboard UI Before the Backend Existed. Here's Exactly How.
Client needed a working demo by morning. Backend engineer was deep in auth. I had designs and an OpenAPI spec. This is the step-by-step of how I built everything in one night.
Test OAuth 2.0 Flows Without an Identity Provider: The Auth Sandbox Approach
Setting up Auth0 or Okta just to test login flows is overkill. Learn how a local auth sandbox gives you working OAuth 2.0 endpoints, real JWTs, and configurable error injection — without a third-party IdP.
Ready to build?
Start deploying serverless functions in under a minute.