The CAPTCHA verification failed during Supabase authentication, typically because the CAPTCHA token was not passed correctly to the auth request or the token has expired. Ensure your frontend is passing the captchaToken in the options parameter and that your Supabase CAPTCHA provider keys are correctly configured.
When Supabase receives an authentication request with CAPTCHA protection enabled, it verifies the CAPTCHA token against your configured provider (hCaptcha or Cloudflare Turnstile). This error occurs when the verification fails, which usually means either the token is missing from the request, the token has already been used or expired, or the CAPTCHA provider's secret key in Supabase is incorrect. The Supabase auth service cannot verify that a human completed the CAPTCHA challenge, so it blocks the authentication request to prevent bot abuse.
Navigate to your Supabase project dashboard and confirm CAPTCHA protection is enabled:
1. Go to Authentication > Bot and Abuse Protection > Enable CAPTCHA protection
2. Select your CAPTCHA provider (hCaptcha or Cloudflare Turnstile)
3. Enter the Secret key from your provider (not the site key)
4. Click Save
5. Copy your Site Key from the provider settings and use it in your frontend
If you haven't signed up with a CAPTCHA provider yet:
- For hCaptcha: https://dashboard.hcaptcha.com
- For Cloudflare Turnstile: https://dash.cloudflare.com/?to=/:account/turnstile
Make sure the Secret key in Supabase exactly matches the key from your provider.
Update your authentication code to include the CAPTCHA token in the options parameter:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(url, key);
// In your sign-up handler
const { data, error } = await supabase.auth.signUp({
email: userEmail,
password: userPassword,
options: {
captchaToken: captchaToken // ← Make sure this is included
}
});
// Or for sign-in
const { data, error } = await supabase.auth.signInWithPassword({
email: userEmail,
password: userPassword,
options: {
captchaToken: captchaToken // ← Must be passed here too
}
});For anonymous sign-ins:
const { data, error } = await supabase.auth.signInAnonymously({
options: {
captchaToken: captchaToken
}
});The captchaToken comes from your CAPTCHA widget's onVerify callback.
Ensure your CAPTCHA widget is properly configured to capture the token. Below are examples for both providers:
hCaptcha example:
import HCaptcha from '@hcaptcha/react-hcaptcha';
import { useRef, useState } from 'react';
function SignUpForm() {
const captchaRef = useRef(null);
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const handleCaptchaVerify = (token: string) => {
setCaptchaToken(token);
};
const handleSignUp = async () => {
if (!captchaToken) {
alert('Please complete the CAPTCHA');
return;
}
const { data, error } = await supabase.auth.signUp({
email: email,
password: password,
options: {
captchaToken: captchaToken
}
});
};
return (
<>
<HCaptcha
ref={captchaRef}
sitekey="your-site-key"
onVerify={handleCaptchaVerify}
/>
<button onClick={handleSignUp}>Sign Up</button>
</>
);
}Cloudflare Turnstile example:
import { Turnstile } from '@captcha/react';
import { useState } from 'react';
function SignUpForm() {
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
return (
<>
<Turnstile
sitekey="your-turnstile-site-key"
onVerify={(token: string) => setCaptchaToken(token)}
/>
<button onClick={() => handleSignUp(captchaToken)}>Sign Up</button>
</>
);
}A common bug is having two variables with the same name in different scopes. For example:
// ❌ WRONG: captchaToken declared in wrong scope
function SignUpForm() {
let captchaToken; // Declared here
function handleCaptchaVerify(token: string) {
const captchaToken = token; // Redeclared in nested scope!
// This only sets the local variable, not the outer one
}
function handleSubmit() {
console.log(captchaToken); // Still undefined!
}
}
// ✓ CORRECT: Use the same variable
function SignUpForm() {
let captchaToken; // Declared once
function handleCaptchaVerify(token: string) {
captchaToken = token; // Assign to outer variable
}
function handleSubmit() {
console.log(captchaToken); // Now has the token
}
}If using React hooks (recommended):
const [captchaToken, setCaptchaToken] = useState<string | null>(null);
const handleCaptchaVerify = (token: string) => {
setCaptchaToken(token); // Updates state for all components
};Check that the Secret key you entered in Supabase exactly matches your CAPTCHA provider's key:
For hCaptcha:
1. Go to https://dashboard.hcaptcha.com
2. Click your site
3. Click Settings
4. Copy the Secret Key (not the Site Key)
5. Paste it into Supabase Auth settings
6. Click Save
For Cloudflare Turnstile:
1. Go to https://dash.cloudflare.com
2. Navigate to Turnstile > Your site
3. Copy the Secret Key
4. Paste it into Supabase Auth settings
5. Click Save
If you recently migrated from hCaptcha to Turnstile, ensure you've updated BOTH the site key in your frontend code AND the secret key in Supabase.
After a CAPTCHA token is used, reset the widget to allow the user to complete it again if needed. If the first attempt fails and they retry, they need a fresh token:
For hCaptcha:
const captchaRef = useRef(null);
const resetCaptcha = () => {
captchaRef.current?.resetCaptcha();
};
const handleSignUp = async () => {
try {
const { error } = await supabase.auth.signUp({
email, password,
options: { captchaToken }
});
if (error) {
resetCaptcha(); // Reset for retry
setCaptchaToken(null);
}
} catch (err) {
resetCaptcha();
}
};For Turnstile:
Turnstile typically resets automatically, but you can force it if needed by unmounting and remounting the component or using its API if provided by the library version you're using.
CAPTCHA providers require valid domains for security. Localhost won't work in production keys. For local testing:
1. Install ngrok: Download from https://ngrok.com
2. Run your dev server: npm run dev (typically on localhost:3000)
3. Start ngrok tunnel:
ngrok http 30004. Update your /etc/hosts file:
sudo nano /etc/hosts
# Add line:
# 127.0.0.1 yourdomain.local5. Add ngrok URL to CAPTCHA provider:
- In hCaptcha dashboard: Add your ngrok URL (e.g., https://xxxx-xxxx-xxxx.ngrok.io)
- In Cloudflare Turnstile: Add your ngrok URL to allowed domains
6. Update your code to use the tunnel URL during local testing
Alternatively, use your CAPTCHA provider's test/dummy keys for localhost development (check their documentation).
Token expiration varies by provider: hCaptcha tokens typically expire after 5 minutes, while Cloudflare Turnstile tokens expire after 5 minutes by default. If users take too long to submit a form after completing CAPTCHA, the token becomes invalid. To handle this, consider regenerating the CAPTCHA challenge if the auth request fails. When migrating between CAPTCHA providers (e.g., hCaptcha to Turnstile), you may see errors like "captcha protection: request disallowed (timeout-or-duplicate)" if old tokens are still being sent. Always update both your frontend site key and Supabase secret key when switching providers. For OAuth flows, CAPTCHA verification happens on the server side during code exchange. Ensure your callback route calls exchangeCodeForSession() which should include CAPTCHA verification if enabled. Some frameworks like Next.js require extra configuration to properly pass the captchaToken through OAuth redirects. Check Supabase logs for detailed error messages—they provide much more information than the API response. If you see "no captcha response (captcha_token) found in request", it definitively means the token was not passed to Supabase.
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