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.
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.
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.
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.
email_address_not_authorized: Email sending to this address is not authorized
Email address not authorized for sending in Supabase Auth
reauthentication_needed: Reauthentication required for security-sensitive actions
Reauthentication required for security-sensitive actions
no_authorization: No authorization header was provided
How to fix "no authorization header was provided" in Supabase
otp_expired: OTP has expired
How to fix 'otp_expired: OTP has expired' in Supabase
bad_oauth_state: OAuth state parameter is missing or invalid
How to fix 'bad_oauth_state: OAuth state parameter missing' in Supabase