How to fix mfa_factor_not_found: MFA factor could not be found in Supabase

SupabaseINTERMEDIATEHIGHUpdated Dec 29, 2025ยท 179 views

The "mfa_factor_not_found" error in Supabase occurs when attempting to challenge, verify, or manage an MFA factor that doesn't exist or is no longer available. This typically happens during incomplete enrollments, page refreshes before verification completes, or when trying to use an unenrolled factor ID.

Quick fix

Fixes mfa_factor_not_found: MFA factor could not be found

async function checkExistingFactors() {
  try {
    const { data: factors, error } = await supabase.auth.mfa.listFactors();

    if (error) {
      console.error('Failed to list factors:', error);
      return;
    }

    console.log('TOTP factors:', factors.totp);
    console.log('Phone factors:', factors.phone);
    console.log('All factors:', factors.all);

    // Check for unverified factors
    const unverified = factors.all.filter(f => f.status !== 'verified');
    console.log('Unverified factors:', unverified.length);

    return factors;
  } catch (err) {
    console.error('Error listing factors:', err);
  }
}

// Call this before enrollment
const currentFactors = await checkExistingFactors();
  1. Check for existing MFA factors before attempting new enrollment
  2. Clean up unverified factors if enrollment limit is reached
  3. Complete the full MFA enrollment flow without interruption
  4. Use challengeAndVerify() helper for simpler one-step verification
supabase โ€” error
async function checkExistingFactors() {
try {
const { data: factors, error } = await supabase.auth.mfa.listFactors();
if (error) {
console.error('Failed to list factors:', error);
return;
}
console.log('TOTP factors:', factors.totp);
console.log('Phone factors:', factors.phone);
console.log('All factors:', factors.all);
// Check for unverified factors
const unverified = factors.all.filter(f => f.status !== 'verified');
console.log('Unverified factors:', unverified.length);
return factors;
} catch (err) {
console.error('Error listing factors:', err);
}
}
// Call this before enrollment
const currentFactors = await checkExistingFactors();
mfa_factor_not_found: MFA factor could not be found

What this error means

The "mfa_factor_not_found" error is a Supabase Auth error that indicates the MFA factor you're trying to access, challenge, or verify does not exist in the system for the currently authenticated user. This error occurs when the `challengeAndVerify()`, `challenge()`, `verify()`, or `unenroll()` API methods receive a factor ID that either: 1. Was never successfully enrolled 2. Was already unenrolled by the user 3. Belongs to a different user 4. Was lost due to session state issues or page refreshes during enrollment In most cases, this error appears during the MFA enrollment flow when the QR code and secret are displayed to the user, but the enrollment is not completed and verified before the page refreshes or times out. The unverified factor becomes orphaned and cannot be challenged or verified later. Supabase has a known limitation where unverified factors can accumulate up to a limit of 10 per user, blocking new factor enrollment attempts if the limit is reached. Understanding how factors are created, verified, and managed is critical for implementing a robust MFA enrollment experience.

Typical symptoms

  • Receive "MFA factor could not be found" or "Factor not found" error when trying to verify MFA during login
  • Error appears after completing MFA enrollment but before verification completes
  • Browser page refresh during MFA setup causes authentication to fail with "factor not found"
  • Cannot complete MFA verification even after entering the correct code from authenticator app
  • Error occurs when trying to challenge an MFA factor that was partially enrolled
  • Mobile app loses session during MFA setup, then shows "factor not found" on restart
  • Getting "MFA enrollment limit reached" even though only 1-2 factors appear in authenticator apps

Common causes

  1. MFA factor enrollment was started but not completed before page refresh or browser close
  2. Factor was successfully enrolled but an error occurred during the verify step
  3. Using an incorrect or expired factor ID in challenge/verify API calls
  4. Multiple unverified MFA factors accumulated from previous failed enrollment attempts
  5. Session was lost or changed after calling enroll() but before verify()
  6. User attempts to verify a factor that belongs to a different authentication session
  7. Unenrolled factor ID is being used to challenge or verify after the factor was deleted

How to fix "mfa_factor_not_found: MFA factor could not be found"

1Check for existing MFA factors before attempting new enrollment

Always list existing factors before starting the enrollment process to understand the current state:

javascript
async function checkExistingFactors() {
  try {
    const { data: factors, error } = await supabase.auth.mfa.listFactors();

    if (error) {
      console.error('Failed to list factors:', error);
      return;
    }

    console.log('TOTP factors:', factors.totp);
    console.log('Phone factors:', factors.phone);
    console.log('All factors:', factors.all);

    // Check for unverified factors
    const unverified = factors.all.filter(f => f.status !== 'verified');
    console.log('Unverified factors:', unverified.length);

    return factors;
  } catch (err) {
    console.error('Error listing factors:', err);
  }
}

// Call this before enrollment
const currentFactors = await checkExistingFactors();

This helps you:
- Understand how many factors the user already has
- Identify unverified factors that may be blocking new enrollments
- Prevent accidental re-enrollment of the same factor

2Clean up unverified factors if enrollment limit is reached

If listFactors() shows unverified factors or you've hit the 10-factor limit, remove unverified factors before starting new enrollment:

javascript
async function cleanupUnverifiedFactors() {
  try {
    const { data: factors, error } = await supabase.auth.mfa.listFactors();

    if (error) {
      console.error('Failed to list factors:', error);
      return;
    }

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

    if (unverified.length === 0) {
      console.log('No unverified factors to clean up');
      return;
    }

    console.log('Cleaning up', unverified.length, 'unverified factors...');

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

      if (unenrollError) {
        console.error('Failed to unenroll factor', factor.id, ':', unenrollError);
      } else {
        console.log('Unenrolled factor:', factor.id);
      }
    }

    console.log('Cleanup complete');
  } catch (err) {
    console.error('Error cleaning up factors:', err);
  }
}

// Call before attempting new enrollment
await cleanupUnverifiedFactors();

This prevents the "factor not found" error caused by orphaned unverified factors.

3Complete the full MFA enrollment flow without interruption

Ensure the complete enrollment flow is done in a single session without page refreshes:

javascript
async function completeMfaEnrollment() {
  try {
    // Step 1: Initiate enrollment
    const { data: enrollData, error: enrollError } = await supabase.auth.mfa.enroll({
      factorType: 'totp',
      friendlyName: 'My Authenticator App'
    });

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

    console.log('Enrollment initiated. Factor ID:', enrollData.id);

    // IMPORTANT: Store the factor ID and secret temporarily
    // Do NOT reload the page or lose this session

    // Step 2: 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 enter this secret:', secret);

    // Step 3: Wait for user to scan and get a code
    // This MUST happen in the same session without page reload
    const userCode = prompt('Enter the 6-digit code from your authenticator app:');

    if (!userCode) {
      console.error('User cancelled enrollment');
      // Clean up the unverified factor
      await supabase.auth.mfa.unenroll({ factorId: enrollData.id });
      return;
    }

    // Step 4: Create a challenge for verification
    const { data: challengeData, error: challengeError } =
      await supabase.auth.mfa.challenge({ factorId: enrollData.id });

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

    // Step 5: Verify the enrollment
    const { data: verifyData, error: verifyError } = await supabase.auth.mfa.verify({
      factorId: enrollData.id,
      challengeId: challengeData.id,
      code: userCode
    });

    if (verifyError) {
      console.error('Verification failed:', verifyError.message);
      // Clean up the unverified factor
      await supabase.auth.mfa.unenroll({ factorId: enrollData.id });
      return;
    }

    console.log('MFA enrollment successful!');
    console.log('Session updated:', verifyData);
  } catch (err) {
    console.error('MFA enrollment exception:', err);
  }
}

Key points:
- Do NOT refresh the page during enrollment
- Keep the factor ID available throughout the flow
- Clean up on error to prevent orphaned factors

4Use challengeAndVerify() helper for simpler one-step verification

For TOTP factors, use the challengeAndVerify() helper which combines challenge and verify:

javascript
async function simpleMfaEnrollment() {
  try {
    // Step 1: Enroll the factor
    const { data: enrollData, error: enrollError } = await supabase.auth.mfa.enroll({
      factorType: 'totp',
      friendlyName: 'My Authenticator'
    });

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

    // Step 2: Display QR code
    console.log('QR Code:', enrollData.totp.qr_code);
    const userCode = prompt('Enter code from authenticator:');

    // Step 3: Use challengeAndVerify for one-step verification
    const { data, error } = await supabase.auth.mfa.challengeAndVerify({
      factorId: enrollData.id,
      code: userCode
    });

    if (error) {
      console.error('Verification failed:', error.message);
      // Clean up unverified factor
      await supabase.auth.mfa.unenroll({ factorId: enrollData.id });
      return;
    }

    console.log('MFA enrollment complete!');
    console.log('Session:', data.session);
  } catch (err) {
    console.error('Error:', err);
  }
}

The challengeAndVerify() helper is simpler and less error-prone than the two-step approach.

5Implement error recovery and retry logic

Handle "factor not found" errors gracefully with retry logic and cleanup:

javascript
async function robustMfaVerification(factorId, code) {
  try {
    // First, verify the factor still exists
    const { data: factors, error: listError } = await supabase.auth.mfa.listFactors();

    if (listError) {
      throw new Error('Failed to list factors: ' + listError.message);
    }

    const factorExists = factors.all.some(f => f.id === factorId);

    if (!factorExists) {
      console.error('Factor not found. Available factors:', factors.all.length);

      // If no factors available, suggest re-enrollment
      if (factors.all.length === 0) {
        throw new Error('No MFA factors found. Please enroll again.');
      }

      // If other factors exist, suggest using them
      throw new Error('The selected factor no longer exists. Please select another factor or re-enroll.');
    }

    // Proceed with verification
    const { data: challengeData, error: challengeError } =
      await supabase.auth.mfa.challenge({ factorId });

    if (challengeError) {
      console.error('Challenge failed:', challengeError.message);
      throw challengeError;
    }

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

    if (error) {
      console.error('Verification failed:', error.message);
      throw error;
    }

    return { success: true, data };
  } catch (err) {
    console.error('MFA verification error:', err.message);
    return {
      success: false,
      error: err.message,
      needsReenroll: err.message.includes('not found')
    };
  }
}

This pattern:
- Checks if the factor exists before attempting verification
- Provides clear error messages to users
- Indicates when re-enrollment is needed

6Persist factor ID safely across page navigations

If you need to split enrollment across multiple pages, store factor state securely:

javascript
// Using localStorage (for non-sensitive development)
async function startEnrollmentWithPersistence() {
  const { data: enrollData, error } = await supabase.auth.mfa.enroll({
    factorType: 'totp'
  });

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

  // Store temporarily (clear after verification or timeout)
  sessionStorage.setItem('mfa_enrollment', JSON.stringify({
    factorId: enrollData.id,
    secret: enrollData.totp.secret,
    qrCode: enrollData.totp.qr_code,
    timestamp: Date.now()
  }));

  // Navigate to confirmation page
  window.location.href = '/mfa/confirm';
}

// On the confirmation page
async function completeEnrollmentWithRetrievedState() {
  const stored = sessionStorage.getItem('mfa_enrollment');

  if (!stored) {
    console.error('Enrollment state lost. Please start again.');
    return;
  }

  const { factorId, timestamp } = JSON.parse(stored);

  // Check timeout (e.g., 5 minutes)
  if (Date.now() - timestamp > 5 * 60 * 1000) {
    console.error('Enrollment session expired. Please start again.');
    sessionStorage.removeItem('mfa_enrollment');
    return;
  }

  const userCode = prompt('Enter code from authenticator:');

  const { data, error } = await supabase.auth.mfa.challengeAndVerify({
    factorId,
    code: userCode
  });

  if (error) {
    console.error('Verification failed:', error);
    // Keep state for retry
    return;
  }

  // Clear state on success
  sessionStorage.removeItem('mfa_enrollment');
  console.log('Enrollment complete!');
}

Use sessionStorage instead of localStorage to avoid persisting sensitive data across browser restarts.

Advanced notes

## Deep Dive: MFA Factor Management in Supabase

### Factor Lifecycle

MFA factors go through distinct states:

1. Unverified (Transient): Created by enroll(), must be verified within the same session
2. Verified (Persistent): Successfully verified and active for authentication
3. Unenrolled: Deleted by user, cannot be used anymore

The "factor not found" error occurs when code tries to access an unverified or unenrolled factor.

### Why Unverified Factors Accumulate

When a user starts enrollment with enroll(), a new unverified factor is created immediately. If they:
- Close the browser
- Refresh the page before verify()
- Abandon the flow midway
- Lose network connectivity

...the factor remains unverified in the database and counts against the 10-factor limit.

Known Issue: Previous Supabase versions didn't automatically clean up unverified factors. Recent updates added auto-deletion of unverified factors, but old orphaned factors may still exist in legacy databases.

### The 10-Factor Limit

Supabase enforces a hard limit of 10 MFA factors per user. This includes both verified AND unverified factors. When the limit is reached:
- New enroll() calls fail with an error
- Users cannot add new MFA methods
- The only solution is to clean up unverified factors with unenroll()

### Factor ID Format and Validity

Factor IDs are UUIDs generated by Supabase Auth. They:
- Are unique per factor
- Do not change once created
- Are only valid for the user who enrolled them
- Cannot be reused after unenrollment

If you see "factor not found" with a UUID you've used before, that factor was likely unenrolled.

### Session State and Factor Verification

MFA verification is session-aware:

javascript
// This fails because factor was created in a different session
const { data: enrollData } = await supabase.auth.mfa.enroll({ factorType: 'totp' });
// [Logout/Login happens here]
const { error } = await supabase.auth.mfa.verify({
  factorId: enrollData.id, // Now invalid - different session
  challengeId: '...',
  code: '123456'
});

Always complete the full enrollment-to-verification flow in a single authenticated session.

### Phone vs TOTP Factor Differences

| Aspect | TOTP | Phone |
|--------|------|-------|
| Enrollment | Requires user to scan QR code and provide verification code | Sends code via SMS/WhatsApp, user provides it |
| Verification | Uses codes from authenticator app | Sends new code via SMS each time |
| "Factor Not Found" | Often due to incomplete enrollment | More likely from session loss/timeout |
| Recovery | Enroll second TOTP as backup | Enroll TOTP factor as backup |

### Debugging Factor Issues

Enable detailed logging to diagnose factor problems:

javascript
// Comprehensive factor diagnostics
async function diagnosticMfaStatus() {
  const { data: { user } } = await supabase.auth.getUser();
  console.log('Authenticated user:', user?.id);

  const { data: factors, error } = await supabase.auth.mfa.listFactors();

  if (error) {
    console.error('Failed to list factors:', error);
    return;
  }

  console.log('=== MFA Factor Status ===');
  console.log('Total factors:', factors.all.length);
  console.log('TOTP factors:', factors.totp.length);
  console.log('Phone factors:', factors.phone.length);

  factors.all.forEach((factor, i) => {
    console.log(`\nFactor ${i + 1}:`, {
      id: factor.id,
      type: factor.factor_type,
      status: factor.status,
      createdAt: factor.created_at,
      updatedAt: factor.updated_at,
      friendlyName: factor.friendly_name
    });
  });

  if (factors.all.length >= 10) {
    console.warn('WARNING: At factor limit (10). Cannot add more factors.');
  }

  const unverified = factors.all.filter(f => f.status !== 'verified');
  if (unverified.length > 0) {
    console.warn('Found', unverified.length, 'unverified factors that should be cleaned up');
  }
}

// Run this to understand current state
await diagnosticMfaStatus();

### Best Practices for Reliable MFA

1. Always list factors first: Check current state before attempting operations
2. Clean up on error: Unenroll unverified factors if verification fails
3. Set timeouts: If enrollment isn't completed within 5-10 minutes, auto-cleanup
4. Use challengeAndVerify: Simpler than the two-step challenge+verify flow
5. Implement account recovery: Have a process for users who lose all MFA access
6. Monitor factor churn: Track enrollment success/failure rates to catch UX issues
7. Document user responsibilities: Tell users not to scan multiple QR codes for the same factor

### Common "Factor Not Found" Scenarios

| Scenario | Cause | Solution |
|----------|-------|----------|
| Enrollment started, page refreshed before verify | Unverified factor lost | List factors and clean up orphaned ones |
| User logs out during verification | Session ended | Start enrollment from scratch |
| Mobile app crashes during setup | Partial enrollment left behind | Clean up on app restart |
| Using wrong factor ID from cache | Old/wrong factor ID | Always get fresh factor list before use |
| User deleted factor via dashboard | Factor unenrolled | Suggest user re-enroll |
| Hitting 10-factor limit | Too many unverified factors | Clean up unverified ones first |

Frequently asked questions

What does "mfa_factor_not_found: MFA factor could not be found" mean?
The "mfa_factor_not_found" error is a Supabase Auth error that indicates the MFA factor you're trying to access, challenge, or verify does not exist in the system for the currently authenticated user. This error occurs when the challengeAndVerify(), challenge(), verify(), or unenroll() API methods receive a factor ID that either: 1. Was never successfully enrolled 2.
How do I fix "mfa_factor_not_found: MFA factor could not be found"?
The "mfa_factor_not_found" error in Supabase occurs when attempting to challenge, verify, or manage an MFA factor that doesn't exist or is no longer available. This typically happens during incomplete enrollments, page refreshes before verification completes, or when trying to use an unenrolled factor ID.
What causes "mfa_factor_not_found: MFA factor could not be found"?
MFA factor enrollment was started but not completed before page refresh or browser close Factor was successfully enrolled but an error occurred during the verify step Using an incorrect or expired factor ID in challenge/verify API calls
Is the "mfa_factor_not_found: MFA factor could not be found" 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