How to fix mfa_verification_rejected: MFA verification was rejected in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 18, 2025· 115 views

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.

Quick fix

Fixes mfa_verification_rejected

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;
  }
}
  1. Verify the TOTP code is current and entered correctly
  2. Check device time synchronization
  3. Implement proper error handling with retry logic
  4. Review custom MFA verification hooks
supabase — error
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;
}
}
mfa_verification_rejected: MFA verification was rejected

What this error means

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.

Typical symptoms

  • Users cannot complete the MFA flow after entering their TOTP code
  • Authentication fails with "mfa_verification_rejected" error after supabase.auth.mfa.verify() call
  • Users are redirected back to the MFA verification screen repeatedly
  • Error appears even when the authenticator app shows a valid code
  • MFA verification works for some users but fails for others

Common causes

  1. User entered an incorrect or expired TOTP code from their authenticator app
  2. Device time is not synchronized with NTP servers, causing TOTP codes to be generated for the wrong time window
  3. Rate limiting triggered from too many verification attempts in a short period (default: once per 2 seconds)
  4. Custom MFA verification hook is rejecting the attempt based on business logic (IP restrictions, attempt limits, etc.)
  5. Code was entered too late after being generated, typically after 30 seconds
  6. Challenge ID from supabase.auth.mfa.challenge() has expired or is invalid
  7. User has reached the maximum number of verification attempts configured in custom hooks
  8. Network latency caused the verification request to arrive outside the valid time window

How to fix "mfa_verification_rejected: MFA verification was rejected"

1Verify the TOTP code is current and entered correctly

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.

2Check device time synchronization

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).

3Implement proper error handling with retry logic

Add appropriate error handling to inform users about the rejection and allow them to retry:

typescript
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:

typescript
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);
}
4Review custom MFA verification hooks

If you've implemented MFA verification hooks, check if they're rejecting legitimate attempts:

typescript
// 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.

5Generate a new challenge if the previous one expired

Challenge IDs expire after a certain period. If a user takes too long to enter their code, generate a fresh challenge:

typescript
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.

6Allow users to re-enroll MFA if consistently failing

If a user consistently cannot verify, provide an option to reset their MFA enrollment:

typescript
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.

Advanced notes

### 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:

json
{
  "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.

Frequently asked questions

What does "mfa_verification_rejected" mean?
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.
How do I fix "mfa_verification_rejected"?
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.
What causes "mfa_verification_rejected"?
User entered an incorrect or expired TOTP code from their authenticator app Device time is not synchronized with NTP servers, causing TOTP codes to be generated for the wrong time window Rate limiting triggered from too many verification attempts in a short period (default: once per 2 seconds)
Is the "mfa_verification_rejected" error serious?
Yes. This is a high-severity error that usually blocks work until it is fixed, but the steps above resolve it.

Official resources & further reading