How to fix mfa_verification_failed: MFA verification failed in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 22, 2025· 117 views

The "mfa_verification_failed: MFA verification failed" error occurs when Supabase Auth cannot verify a multi-factor authentication code. This typically happens due to incorrect TOTP codes, timing synchronization issues, expired codes, or rate limiting from too many failed verification attempts.

Quick fix

Fixes mfa_verification_failed: MFA verification failed

// Example: Proper MFA verification flow
const { data, error } = await supabase.auth.mfa.verify({
  factorId: factorId,
  challengeId: challengeId,
  code: '123456' // Enter fresh code immediately after generation
});

if (error) {
  console.error('MFA verification failed:', error.message);
  // Prompt user to try again with a fresh code
}
  1. Verify you are using the correct authenticator app and account
  2. Wait for a fresh code and enter it immediately
  3. Check time synchronization on your device
  4. Clear failed attempts and retry MFA verification
supabase — error
// Example: Proper MFA verification flow
const { data, error } = await supabase.auth.mfa.verify({
factorId: factorId,
challengeId: challengeId,
code: '123456' // Enter fresh code immediately after generation
});
if (error) {
console.error('MFA verification failed:', error.message);
// Prompt user to try again with a fresh code
}
mfa_verification_failed: MFA verification failed

What this error means

The "mfa_verification_failed" error is a Supabase Auth error code that indicates the multi-factor authentication (MFA) verification process has failed. This error occurs during the sign-in flow when a user has MFA enabled and attempts to verify their identity with a time-based one-time password (TOTP) from an authenticator app or a code sent via SMS/WhatsApp. When Supabase Auth receives an MFA verification request through the `supabase.auth.mfa.verify()` API, it compares the submitted code against the expected code generated by the user's enrolled factor. If the codes don't match, the verification fails and returns this error. The verification can fail for several reasons: incorrect code entry, time synchronization issues between the server and authenticator app, expired codes, or security measures like rate limiting that block excessive verification attempts. This error is particularly important because it prevents users from completing authentication, even if they've successfully entered their primary credentials (email/password). Understanding the root cause is essential for maintaining a smooth user authentication experience while preserving security.

Typical symptoms

  • Users receive "Invalid MFA Code" or "MFA verification failed" error message when entering TOTP codes
  • Authentication fails after entering primary credentials, blocking users from accessing the application
  • Verification works intermittently, succeeding sometimes and failing other times with the same code
  • Error appears in Supabase Auth logs showing multiple failed MFA verification attempts
  • Users report that codes from their authenticator app are always rejected

Common causes

  1. Incorrect verification code entered by the user (typo or wrong code from authenticator app)
  2. Time synchronization mismatch between the server and the authenticator app (TOTP requires accurate time)
  3. Code has expired before verification (TOTP codes typically expire every 30 seconds)
  4. User is entering the code more than 3 seconds after it changed in the authenticator app
  5. Rate limiting triggered due to too many failed verification attempts in a short period
  6. MFA factor not properly enrolled or enrollment process was incomplete
  7. Multiple unverified MFA factors accumulated, causing authentication confusion

How to fix "mfa_verification_failed: MFA verification failed"

1Verify you are using the correct authenticator app and account

First, ensure the user is looking at the correct TOTP code:

1. Open your authenticator app (Google Authenticator, Authy, Microsoft Authenticator, etc.)
2. Find the correct entry for your Supabase application
3. Verify the account name matches your logged-in user
4. Ensure you're not looking at a different account's code

If you have multiple accounts or MFA factors enrolled, it's easy to enter the wrong code. Double-check the account label in your authenticator app.

2Wait for a fresh code and enter it immediately

TOTP codes expire every 30 seconds, and timing is critical:

1. Watch the timer in your authenticator app (usually a circular countdown)
2. Wait for a new code to appear (don't use a code that's about to expire)
3. Enter the code immediately after it appears
4. Submit within 5-10 seconds for best results

A known issue with Supabase MFA is that codes may be rejected if more than 3 seconds have passed since the code changed. For best results, enter the code as soon as it appears in your authenticator app.

javascript
// Example: Proper MFA verification flow
const { data, error } = await supabase.auth.mfa.verify({
  factorId: factorId,
  challengeId: challengeId,
  code: '123456' // Enter fresh code immediately after generation
});

if (error) {
  console.error('MFA verification failed:', error.message);
  // Prompt user to try again with a fresh code
}
3Check time synchronization on your device

TOTP relies on accurate time synchronization between the server and your device:

For iOS:
1. Go to Settings → General → Date & Time
2. Enable "Set Automatically"
3. Restart your authenticator app

For Android:
1. Go to Settings → System → Date & time
2. Enable "Use network-provided time"
3. Open Google Authenticator → Settings → Time correction for codes → Sync now

For Desktop:
- Ensure your system clock is set to automatic/network time
- Verify your timezone is correct

Even a few seconds of time drift can cause TOTP verification to fail. Most authenticator apps require time synchronization within 30-90 seconds of the server time.

4Clear failed attempts and retry MFA verification

If you've had multiple failed attempts, rate limiting may be blocking verification:

1. Wait 1-2 minutes before trying again to allow rate limits to reset
2. Close and reopen your authenticator app
3. Generate a new challenge before verifying:

javascript
// Start fresh with a new challenge
async function retryMfaVerification(factorId) {
  try {
    // Step 1: Create a new challenge
    const { data: challengeData, error: challengeError } =
      await supabase.auth.mfa.challenge({ factorId });

    if (challengeError) {
      console.error('Challenge creation failed:', challengeError);
      return;
    }

    // Step 2: Wait for user to enter fresh TOTP code
    const totpCode = prompt('Enter 6-digit code from authenticator app:');

    // Step 3: Verify with the new challenge
    const { data, error } = await supabase.auth.mfa.verify({
      factorId: factorId,
      challengeId: challengeData.id,
      code: totpCode
    });

    if (error) {
      console.error('Verification failed:', error.message);
      // Suggest waiting and trying again
    } else {
      console.log('MFA verification successful!');
    }
  } catch (err) {
    console.error('MFA retry failed:', err);
  }
}

If verification continues to fail, the MFA factor may need to be re-enrolled.

5Re-enroll the MFA factor if issues persist

If verification continues to fail despite correct codes, re-enroll the MFA factor:

javascript
// Step 1: Unenroll the existing factor (requires authentication)
async function reenrollMfa() {
  // Get existing factors
  const { data: factors } = await supabase.auth.mfa.listFactors();

  // Unenroll problematic factor
  const problematicFactor = factors.totp[0]; // Or identify the specific factor
  const { error: unenrollError } = await supabase.auth.mfa.unenroll({
    factorId: problematicFactor.id
  });

  if (unenrollError) {
    console.error('Unenroll failed:', unenrollError);
    return;
  }

  // Step 2: Enroll a new factor
  const { data: enrollData, error: enrollError } =
    await supabase.auth.mfa.enroll({
      factorType: 'totp',
      friendlyName: 'My Authenticator App'
    });

  if (enrollError) {
    console.error('Enrollment failed:', enrollError);
    return;
  }

  // Step 3: Display QR code to user
  const qrCode = enrollData.totp.qr_code;
  const secret = enrollData.totp.secret;

  console.log('Scan this QR code with your authenticator app:');
  console.log(qrCode);
  console.log('Or manually enter secret:', secret);

  // Step 4: Verify the newly enrolled factor
  const totpCode = prompt('Enter code from authenticator app:');

  const { data: challengeData } = await supabase.auth.mfa.challenge({
    factorId: enrollData.id
  });

  const { error: verifyError } = await supabase.auth.mfa.verify({
    factorId: enrollData.id,
    challengeId: challengeData.id,
    code: totpCode
  });

  if (verifyError) {
    console.error('New factor verification failed:', verifyError);
  } else {
    console.log('MFA re-enrollment successful!');
  }
}

Important: Make sure to delete the old entry from your authenticator app and scan the new QR code.

6Implement MFA verification hooks for better error handling

For production applications, implement MFA verification hooks to handle failures gracefully:

javascript
// Example: MFA verification with comprehensive error handling
async function secureMfaVerification(factorId, challengeId, code) {
  const MAX_ATTEMPTS = 3;
  const LOCKOUT_DURATION = 60000; // 1 minute

  try {
    const { data, error } = await supabase.auth.mfa.verify({
      factorId,
      challengeId,
      code
    });

    if (error) {
      // Track failed attempts
      const attempts = incrementFailedAttempts(factorId);

      if (attempts >= MAX_ATTEMPTS) {
        return {
          success: false,
          error: 'Too many failed attempts. Please wait 1 minute and try again.',
          lockout: true
        };
      }

      return {
        success: false,
        error: 'Invalid code. Please try again with a fresh code.',
        attemptsRemaining: MAX_ATTEMPTS - attempts
      };
    }

    // Reset failed attempts on success
    clearFailedAttempts(factorId);

    return {
      success: true,
      data: data
    };
  } catch (err) {
    console.error('MFA verification exception:', err);
    return {
      success: false,
      error: 'An unexpected error occurred. Please try again.'
    };
  }
}

// Helper functions for rate limiting (implement with Redis or in-memory store)
function incrementFailedAttempts(factorId) {
  // Store in Redis or session storage
  // Return current count
}

function clearFailedAttempts(factorId) {
  // Clear stored attempts
}

You can also implement server-side MFA verification hooks using Supabase Edge Functions for centralized rate limiting and logging.

Advanced notes

## Deep Dive: TOTP and MFA Security in Supabase

### How TOTP Works

Time-based One-Time Passwords (TOTP) use a shared secret and the current time to generate codes:

1. Shared Secret: During enrollment, Supabase generates a secret key shared between the server and the user's authenticator app
2. Time Window: Codes are generated in 30-second windows
3. Algorithm: Uses HMAC-SHA1 to combine the secret and current time
4. Validation Window: Supabase accepts codes from the current window and typically 1 window before/after (90 seconds total)

### Known Timing Issues

Supabase has a documented timing sensitivity where codes may be rejected if more than 3 seconds have passed since generation. This is stricter than the standard TOTP specification and can cause user frustration. The recommended workaround is to enter codes immediately after they appear in the authenticator app.

### MFA Verification Hooks

Supabase provides MFA verification hooks (server-side functions) that allow you to:

- Rate Limiting: Limit verification attempts to prevent brute-force attacks
- Logging: Track all MFA verification attempts for security auditing
- Custom Logic: Implement additional checks (device fingerprinting, location verification)
- User Lockout: Temporarily lock accounts after too many failed attempts

Example hook configuration:

sql
-- Create an Edge Function for MFA verification hook
create function public.handle_mfa_verification()
returns trigger as $$
begin
  -- Log the verification attempt
  insert into mfa_logs (user_id, factor_id, success, timestamp)
  values (new.user_id, new.factor_id, new.success, now());

  -- Implement rate limiting logic
  -- Block if more than 5 failed attempts in last 5 minutes

  return new;
end;
$$ language plpgsql security definer;

### Security Best Practices

1. Backup Factors: Always enroll at least two MFA factors (e.g., two authenticator apps or one authenticator + backup codes)
2. Recovery Options: Supabase doesn't provide recovery codes by default, so implement your own recovery mechanism
3. Account Recovery: Plan for scenarios where users lose access to their authenticator app (support process, identity verification)
4. Factor Management: Allow users to manage their enrolled factors through a settings page

### Phone MFA vs TOTP MFA

Supabase supports two types of MFA:

| Factor Type | Pros | Cons |
|------------|------|------|
| TOTP (Authenticator App) | More secure, works offline, no per-message cost | Requires app installation, time sync issues |
| Phone (SMS/WhatsApp) | Easier for users, no app required | Costs per SMS, carrier dependency, less secure |

The mfa_verification_failed error applies to both types, but the troubleshooting steps differ slightly for phone-based MFA.

### Debugging MFA Issues

Enable detailed logging to diagnose persistent MFA issues:

1. Client-side logging:

javascript
supabase.auth.onAuthStateChange((event, session) => {
  console.log('Auth event:', event);
  if (event === 'MFA_CHALLENGE_VERIFIED') {
    console.log('MFA verified successfully');
  }
});

2. Check Supabase Dashboard: Authentication → Logs → Filter for "mfa_verification_failed"
3. Error details: The error response includes status, code, and sometimes provider-specific details

### Unverified Factor Accumulation

A known issue is that unverified MFA factors can accumulate when users start enrollment but don't complete it. This can cause confusion during authentication. Clean up unverified factors periodically:

javascript
// List all factors
const { data: factors } = await supabase.auth.mfa.listFactors();

// Find unverified factors
const unverified = factors.all.filter(f => f.status !== 'verified');

// Unenroll them
for (const factor of unverified) {
  await supabase.auth.mfa.unenroll({ factorId: factor.id });
}

Frequently asked questions

What does "mfa_verification_failed: MFA verification failed" mean?
The "mfa_verification_failed" error is a Supabase Auth error code that indicates the multi-factor authentication (MFA) verification process has failed. This error occurs during the sign-in flow when a user has MFA enabled and attempts to verify their identity with a time-based one-time password (TOTP) from an authenticator app or a code sent via SMS/WhatsApp. verify() API, it compares the submitted code against the expected code generated by the user's enrolled factor.
How do I fix "mfa_verification_failed: MFA verification failed"?
The "mfa_verification_failed: MFA verification failed" error occurs when Supabase Auth cannot verify a multi-factor authentication code. This typically happens due to incorrect TOTP codes, timing synchronization issues, expired codes, or rate limiting from too many failed verification attempts.
What causes "mfa_verification_failed: MFA verification failed"?
Incorrect verification code entered by the user (typo or wrong code from authenticator app) Time synchronization mismatch between the server and the authenticator app (TOTP requires accurate time) Code has expired before verification (TOTP codes typically expire every 30 seconds)
Is the "mfa_verification_failed: MFA verification failed" 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