How to fix bad_code_verifier: PKCE flow code verifier does not match in Supabase
This error occurs during Supabase authentication when the code_verifier stored from the initial PKCE request doesn't match the one sent during token exchange. It typically indicates the code verifier wasn't persisted correctly, or the authentication request was completed on a different browser/device than where it started.
Quick fix
Fixes bad_code_verifier: PKCE flow code verifier does not match
// Correct approach - Supabase handles PKCE internally
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourdomain.com/auth/callback',
},
});- Use Supabase's built-in signInWithOAuth method
- Verify redirect URL matches exactly
- Ensure code exchange happens on same browser/device
- For Next.js/SSR, use proper cookie configuration
// Correct approach - Supabase handles PKCE internallyconst { data, error } = await supabase.auth.signInWithOAuth({provider: 'google',options: {redirectTo: 'https://yourdomain.com/auth/callback',},});bad_code_verifier: PKCE flow code verifier does not match
What this error means
PKCE (Proof Key for Code Exchange) is a security extension to OAuth 2.0 that protects against authorization code interception attacks. When initiating the PKCE flow, your application generates a random code_verifier and sends a code_challenge (hashed version) to the authorization server. During the token exchange, you must send back the original code_verifier so the server can verify it matches the code_challenge. When you see "bad_code_verifier: PKCE flow code verifier does not match," it means the code_verifier you're sending during token exchange doesn't match what was originally sent during authorization. This is a security-critical check that prevents attackers from hijacking the authentication flow.
Typical symptoms
- Authentication fails with 'bad_code_verifier' error message
- Token exchange fails even though you have a valid authorization code
- Error occurs when calling supabase.auth.exchangeCodeForSession() or similar token exchange method
- Works when testing in browser but fails in CI/CD pipeline
- Works on initial login but fails on password reset flow
Common causes
- The code_verifier is not being persisted in session storage, local storage, or cookies between the authorization request and token exchange
- Requesting authorization and exchanging the code on different browsers or devices (code_verifier is device-specific)
- The authorization code expired (valid for only 5 minutes) before token exchange attempt
- Client reinitialization between authorization and token exchange, causing loss of stored code_verifier
- Incorrect implementation of PKCE flow manually instead of using Supabase's built-in auth methods
- Missing or improperly configured redirect URL matching between Supabase settings and application
- Storage unavailability in server-side rendering (SSR) contexts where localStorage/sessionStorage aren't available
How to fix "bad_code_verifier: PKCE flow code verifier does not match"
Instead of manually implementing the PKCE flow, rely on Supabase's built-in authentication. This automatically handles code_verifier generation and storage:
// Correct approach - Supabase handles PKCE internally
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourdomain.com/auth/callback',
},
});This method automatically:
- Generates a secure code_verifier
- Creates the code_challenge
- Stores the code_verifier in the browser
- Handles token exchange correctly
In Supabase dashboard, check that your configured redirect URLs match exactly what you're using in code:
1. Go to Supabase Dashboard > Authentication > URL Configuration
2. Add your callback URL (e.g., https://yourdomain.com/auth/callback)
3. Note: https://yourdomain.com and https://www.yourdomain.com are different URLs
4. Ensure the redirectTo parameter in signInWithOAuth matches exactly:
const { data, error } = await supabase.auth.signInWithOAuth({
provider: 'google',
options: {
redirectTo: 'https://yourdomain.com/auth/callback', // Must match dashboard exactly
},
});A mismatch causes the code_verifier to not be properly stored or retrieved.
The PKCE code_verifier is stored locally and device-specific. Both authorization and token exchange must happen on the same browser:
// Callback route - exchange code for session on same browser
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
// This must happen on the same browser where signInWithOAuth was called
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
if (error) {
console.error('Code exchange failed:', error.message);
}
return NextResponse.redirect('/dashboard');
}Don't try to:
- Call signInWithOAuth on one device and exchangeCodeForSession on another
- Store the authorization code and complete the flow later in a different session
- Handle the callback on a different domain than your app
In server-side rendering contexts, localStorage isn't available. Configure cookies properly:
// In your Next.js route handler
import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
import { cookies } from 'next/headers';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const code = searchParams.get('code');
// Use the auth helpers client which properly handles cookies
const supabase = createRouteHandlerClient({ cookies });
if (code) {
// This will properly store the session in cookies
await supabase.auth.exchangeCodeForSession(code);
}
return NextResponse.redirect('/dashboard');
}The auth helpers library automatically:
- Handles PKCE flow correctly
- Stores code_verifier in secure cookies
- Manages code exchange properly in SSR contexts
Password reset flows sometimes fail with code_verifier mismatch. Use the token_hash approach:
1. In your Supabase email template, use the token_hash:
{{ .ConfirmationURL }}
2. Parse the token_hash from the URL in your reset callback:
const { searchParams } = new URL(request.url);
const token_hash = searchParams.get('token_hash');
const type = searchParams.get('type'); // 'recovery' for password reset
// Use verifyOtp for password reset instead of exchangeCodeForSession
const { data, error } = await supabase.auth.verifyOtp({
email: userEmail,
token: token_hash,
type: 'recovery',
});3. This avoids PKCE code_verifier issues entirely for password resets.
Authorization codes are only valid for 5 minutes. If too much time passes, the code expires:
1. Start the PKCE flow and immediately handle the callback
2. Don't delay the token exchange - complete it within 5 minutes
3. If code expires, the user must restart authentication:
const { data, error } = await supabase.auth.exchangeCodeForSession(code);
if (error?.message.includes('expired') || error?.message.includes('invalid')) {
// Code has expired, restart authentication
console.log('Auth code expired, please try logging in again');
// Redirect user to login
}Monitor the time between signInWithOAuth call and token exchange to ensure it's under 5 minutes.
Advanced notes
PKCE Security Details: The code_verifier is a random 43-128 character string, and code_challenge is the SHA256 hash encoded in Base64-URL format. This design prevents authorization code interception because even if an attacker intercepts the code, they can't complete the token exchange without the original code_verifier.
Manual PKCE Implementation: While possible to implement PKCE manually in cases where you need custom control, it's error-prone. Only attempt this if Supabase's built-in methods don't meet your requirements, and ensure you:
- Generate cryptographically secure random code_verifier (crypto.getRandomValues)
- Use proper Base64-URL encoding (not standard Base64)
- Hash with SHA256 for code_challenge
- Store code_verifier securely (encrypted cookies for SSR)
- Don't recreate the Supabase client between authorization and exchange
Docker/Self-Hosted Issues: Some self-hosted Supabase instances may have issues persisting code_verifier if auth service configuration is incorrect. Ensure SESSION_JWT_SECRET and related auth environment variables are set properly.
CI/CD Pipelines: If authentication fails in CI/CD (GitHub Actions, etc.), you likely can't complete the interactive PKCE flow. Use service role keys or implement a custom authentication flow designed for non-interactive environments instead.
Frequently asked questions
- What does "bad_code_verifier: PKCE flow code verifier does not match" mean?
- 0 that protects against authorization code interception attacks. When initiating the PKCE flow, your application generates a random code_verifier and sends a code_challenge (hashed version) to the authorization server. During the token exchange, you must send back the original code_verifier so the server can verify it matches the code_challenge.
- How do I fix "bad_code_verifier: PKCE flow code verifier does not match"?
- This error occurs during Supabase authentication when the code_verifier stored from the initial PKCE request doesn't match the one sent during token exchange. It typically indicates the code verifier wasn't persisted correctly, or the authentication request was completed on a different browser/device than where it started.
- What causes "bad_code_verifier: PKCE flow code verifier does not match"?
- The code_verifier is not being persisted in session storage, local storage, or cookies between the authorization request and token exchange Requesting authorization and exchanging the code on different browsers or devices (code_verifier is device-specific) The authorization code expired (valid for only 5 minutes) before token exchange attempt
- Is the "bad_code_verifier: PKCE flow code verifier does not match" error serious?
- Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.