All Posts
PKCEOAuth 2.0Frontend

PKCE OAuth Flow Testing: A Step-by-Step Guide for Frontend Developers

Kiran MayeeApril 20, 202510 min read

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:

  1. The client generates a random code_verifier (43-128 character string).
  2. The client computes code_challenge = BASE64URL(SHA256(code_verifier)).
  3. The client sends the code_challenge with the authorization request.
  4. The server stores the challenge alongside the authorization code.
  5. When the client exchanges the code for tokens, it sends the original code_verifier.
  6. The server computes SHA256(code_verifier) and compares it to the stored challenge.
  7. 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 plain code challenge method sends the verifier as-is, defeating the purpose. Always use S256.
  • Not testing verifier mismatch — your frontend should handle the invalid_grant error 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 state parameter 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.

Share this article:

About the Author

Kiran Mayee

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.

Ready to build?

Start deploying serverless functions in under a minute.

Get Started Free