This error occurs when Supabase Auth rejects a multi-factor authentication (MFA) verification attempt, typically due to invalid codes, rate limiting, or custom verification hooks blocking the authentication flow.
The `mfa_verification_rejected` error is returned by Supabase Auth when a multi-factor authentication verification attempt fails validation checks. This happens during the MFA challenge-verify flow after a user has successfully entered their primary credentials (email/password) but before gaining full access to their account. Supabase Auth uses time-based one-time passwords (TOTP) or phone-based verification codes as the second authentication factor. When you call `supabase.auth.mfa.verify()` with a code, the authentication service validates it against several criteria: the code must be correct for the current time window, not expired, submitted within rate limits, and pass any custom verification hooks you've configured. The rejection can originate from the built-in validation logic (incorrect codes, timing issues) or from custom MFA verification hooks that implement additional security policies like attempt limiting, device fingerprinting, or geographic restrictions.
First, confirm the user is entering the code displayed in their authenticator app at the moment of submission. TOTP codes typically expire every 30 seconds.
Ask the user to:
1. Wait for a fresh code to appear in their authenticator app
2. Enter the complete 6-digit code immediately
3. Avoid copying/pasting which may introduce extra spaces
If testing yourself, try multiple consecutive codes to rule out timing issues.
TOTP codes are generated based on the current time. If the device running the authenticator app has incorrect time, all codes will be invalid.
On user's device:
- Enable automatic time synchronization in system settings
- Verify the time matches an accurate source like time.is
- For Android: Settings > Date & Time > Use network-provided time
- For iOS: Settings > General > Date & Time > Set Automatically
On your server:
Ensure your Supabase project's server time is accurate (this is typically handled by Supabase infrastructure).
Add appropriate error handling to inform users about the rejection and allow them to retry:
async function verifyMFA(code: string, challengeId: string) {
try {
const { data, error } = await supabase.auth.mfa.verify({
factorId: factorId,
challengeId: challengeId,
code: code
});
if (error) {
if (error.message.includes('mfa_verification_rejected')) {
// Provide user-friendly feedback
return {
success: false,
message: 'Verification code was rejected. Please try a fresh code from your authenticator app.',
canRetry: true
};
}
throw error;
}
return { success: true, data };
} catch (err) {
console.error('MFA verification error:', err);
throw err;
}
}Add rate limiting on the client side to prevent rapid-fire attempts:
const [lastAttempt, setLastAttempt] = useState<number>(0);
async function handleVerify(code: string) {
const now = Date.now();
const timeSinceLastAttempt = now - lastAttempt;
if (timeSinceLastAttempt < 2000) {
toast.error('Please wait before trying again');
return;
}
setLastAttempt(now);
await verifyMFA(code, challengeId);
}If you've implemented MFA verification hooks, check if they're rejecting legitimate attempts:
// Example MFA verification hook that may be too strict
export const handler = async (event, context) => {
const { user, factor_id, valid } = event;
// Check if this hook is causing rejections
const attempts = await getRecentAttempts(user.id);
if (attempts > 5) {
return {
decision: 'reject',
message: 'You have exceeded maximum number of MFA attempts.'
};
}
// Add logging to diagnose rejections
console.log('MFA verification attempt:', {
userId: user.id,
factorId: factor_id,
isValid: valid,
attemptCount: attempts
});
return { decision: 'continue' };
};Review the hook logs in your Supabase dashboard under Authentication > Hooks to see rejection reasons.
Challenge IDs expire after a certain period. If a user takes too long to enter their code, generate a fresh challenge:
async function getValidChallenge(factorId: string) {
// Challenge IDs may expire after several minutes
const { data: challenge, error } = await supabase.auth.mfa.challenge({
factorId: factorId
});
if (error) {
throw new Error(`Failed to create MFA challenge: ${error.message}`);
}
return challenge;
}
// Use this in your verification flow
const challenge = await getValidChallenge(factorId);
const result = await verifyMFA(userCode, challenge.id);Consider setting a UI timer to automatically refresh the challenge after 2-3 minutes of inactivity.
If a user consistently cannot verify, provide an option to reset their MFA enrollment:
async function resetMFAEnrollment(userId: string) {
// Admin API call to unenroll the user's factor
const { data: factors } = await supabase.auth.admin.mfa.listFactors({
userId: userId
});
for (const factor of factors) {
await supabase.auth.admin.mfa.deleteFactor({
id: factor.id,
userId: userId
});
}
// User can now re-enroll with a fresh QR code
console.log('MFA enrollment reset for user:', userId);
}This requires the admin API and should be protected with appropriate authentication. Provide this as a support tool or self-service option with identity verification.
### Time Window Considerations
Supabase TOTP verification accepts codes that are valid for a small time window around the current moment (typically ±1 time step, which is 30 seconds). This provides some tolerance for clock drift, but if the device time is off by several minutes, all codes will fail.
### Rate Limiting Architecture
Supabase enforces a minimum 2-second delay between verification attempts to prevent brute-force attacks. This is implemented at the authentication service level and cannot be disabled. Design your UI to inform users of this limitation.
### Custom Hook Response Format
When implementing MFA verification hooks, the rejection response must follow this structure:
{
"decision": "reject",
"message": "Custom rejection message shown to user"
}The message field is returned in the error object and can be displayed to users. Keep messages generic to avoid leaking security information.
### Security Best Practices
- Never log or store the actual TOTP codes users enter
- Implement progressive delays for repeated failures (e.g., exponential backoff)
- Consider implementing account lockout after excessive failures (via hooks)
- Monitor for patterns that might indicate automated attacks
- Use MFA verification hooks to implement IP-based restrictions if needed
### Recovery Codes
Supabase doesn't provide built-in recovery codes for MFA. If you need this feature, implement it at the application level by:
1. Generating single-use backup codes during MFA enrollment
2. Storing hashed versions in your database
3. Providing an alternate verification flow that accepts these codes
### Multi-Device Considerations
Users can have MFA enrolled on multiple devices, but each enrollment creates a separate factor with its own secret. If a user enrolls on a new device without unenrolling the old one, they may have multiple TOTP factors associated with their account. The supabase.auth.mfa.listFactors() API can help manage these.
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