The mfa_factor_name_conflict error occurs when you try to enroll a new MFA factor with a friendly name that's already in use by another factor for the same user. This happens because Supabase enforces unique friendly names to help users distinguish between multiple authentication methods.
The "mfa_factor_name_conflict" error indicates that you're attempting to create a new MFA factor (such as TOTP or phone authentication) with a friendly name that already exists for the current user. Supabase uses friendly names to help users identify which authentication method they're using. Each user can have up to 10 different MFA factors, but each factor must have a unique friendly name within that user's account. This prevents confusion when a user has multiple TOTP authenticators, phone numbers, or other MFA methods enrolled. This error commonly occurs when: - A user tries to set up a second TOTP authenticator with the same name - The application defaults friendly names to the user's email or username instead of unique identifiers - A previous MFA enrollment was cancelled but the factor name wasn't cleaned up - Unverified factors with duplicate names accumulate on the account
First, check what MFA factors are already enrolled, including unverified ones:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// Get all factors for the current user
const { data: factors, error } = await supabase.auth.mfa.listFactors();
if (error) {
console.error('Error listing factors:', error);
} else {
console.log('All factors:');
factors.all.forEach(factor => {
console.log(
`- ${factor.friendly_name} (${factor.factor_type}, Status: ${factor.status})`
);
});
}This shows all TOTP, phone, and other factors, including unverified ones that may be causing the conflict.
Before creating a new factor enrollment, remove any existing unverified factors. This is the most important step:
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
async function cleanupUnverifiedFactors() {
// Get all factors
const { data: factors, error } = await supabase.auth.mfa.listFactors();
if (error) {
console.error('Error listing factors:', error);
return;
}
// Remove all unverified factors
for (const factor of factors.all) {
if (factor.status === 'unverified') {
console.log(`Unenrolling unverified factor: ${factor.friendly_name}`);
const { error: unenrollError } = await supabase.auth.mfa.unenrollFactor(
factor.id
);
if (unenrollError) {
console.error('Error unenrolling factor:', unenrollError);
}
}
}
}
// Clean up before enrolling
await cleanupUnverifiedFactors();
// Now enroll new factor with unique name
const { data: newFactor, error: enrollError } =
await supabase.auth.mfa.enrollFactor({
factorType: 'totp',
friendlyName: 'My Main Authenticator', // Must be unique
});Instead of generic names like email addresses, use descriptive names that distinguish between factors:
// GOOD: Unique, descriptive names
const names = {
firstTOTP: 'Authenticator App - iPhone',
secondTOTP: 'Authenticator App - Work Computer',
phone: 'SMS - +1 555-0123',
backup: 'Recovery Codes',
};
// BAD: Generic or potentially duplicate names
const badNames = {
auth: 'My Email', // Too generic, may conflict
mfa: '[email protected]', // Email might already be a factor
backup: 'My Phone', // Vague
};Ask the user for a unique name during MFA setup:
async function enrollMFAWithUserInput() {
// Clean up first
await cleanupUnverifiedFactors();
// Prompt user for a unique name
const friendlyName = prompt(
'Give this authenticator a unique name (e.g., "iPhone", "Work Computer"):'
);
if (!friendlyName) {
console.error('Friendly name is required');
return;
}
// Validate it doesn't conflict with existing factors
const { data: factors } = await supabase.auth.mfa.listFactors();
const nameExists = factors.all.some(
f => f.friendly_name.toLowerCase() === friendlyName.toLowerCase()
);
if (nameExists) {
alert('That name is already in use. Please choose a different one.');
return;
}
// Proceed with enrollment
const { data: newFactor, error } = await supabase.auth.mfa.enrollFactor({
factorType: 'totp',
friendlyName,
});
if (error) {
console.error('Enrollment failed:', error);
} else {
console.log('Factor enrolled successfully');
}
}Create a complete MFA enrollment flow that handles the mfa_factor_name_conflict error:
async function enrollMFAWithErrorHandling(friendlyName) {
try {
// Step 1: Clean up unverified factors
const { data: factors } = await supabase.auth.mfa.listFactors();
for (const factor of factors.all) {
if (factor.status === 'unverified') {
await supabase.auth.mfa.unenrollFactor(factor.id);
}
}
// Step 2: Attempt to enroll with the provided name
const { data: enrollData, error: enrollError } =
await supabase.auth.mfa.enrollFactor({
factorType: 'totp',
friendlyName,
});
if (enrollError) {
if (enrollError.message.includes('mfa_factor_name_conflict')) {
throw new Error(
`The name "${friendlyName}" is already in use. Please choose a different name.`
);
}
throw enrollError;
}
// Step 3: Display QR code to user
if (enrollData?.totp?.qr_code) {
console.log('QR Code:', enrollData.totp.qr_code);
// Display in UI for user to scan
}
// Step 4: Wait for user to verify
return enrollData;
} catch (error) {
console.error('MFA enrollment failed:', error.message);
// Show error message to user and let them retry with different name
throw error;
}
}
// Usage
try {
await enrollMFAWithErrorHandling('iPhone Authenticator');
} catch (error) {
console.error('Setup failed:', error.message);
// Show retry UI to user
}When users cancel MFA setup, unenroll the unverified factor:
async function cancelMFASetup() {
// Get all unverified factors
const { data: factors } = await supabase.auth.mfa.listFactors();
// Unenroll any unverified factors (cleanup from cancelled flow)
for (const factor of factors.all) {
if (factor.status === 'unverified') {
console.log(`Removing unverified factor: ${factor.friendly_name}`);
await supabase.auth.mfa.unenrollFactor(factor.id);
}
}
console.log('MFA setup cancelled and cleaned up');
}
// Wire this to cancel button
document.getElementById('cancel-mfa-btn').addEventListener('click', () => {
cancelMFASetup();
});This prevents unverified factors from accumulating and causing name conflicts on subsequent attempts.
If you must auto-generate friendly names, use unique identifiers:
import { v4 as uuidv4 } from 'uuid';
// Option 1: Use timestamp (less readable but unique)
function generateFriendlyName(type) {
const now = new Date();
const timestamp = now.toLocaleString();
return `${type} - ${timestamp}`;
}
// Option 2: Use device info + timestamp (more readable)
function generateFriendlyNameWithDevice(type) {
const ua = navigator.userAgent;
const isChrome = /Chrome/.test(ua) ? 'Chrome' : '';
const isSafari = /Safari/.test(ua) && !/Chrome/.test(ua) ? 'Safari' : '';
const browser = isChrome || isSafari || 'Unknown';
return `${type} - ${browser} - ${Date.now()}`;
}
// Option 3: Use UUID (most unique, less readable)
function generateFriendlyNameWithUUID(type) {
const shortId = uuidv4().split('-')[0];
return `${type} (${shortId})`;
}
// Usage
const name = generateFriendlyNameWithDevice('TOTP');
console.log(name); // 'TOTP - Chrome - 1735454921000'This ensures no name conflicts, though users won't be able to easily identify their factors.
Test that MFA enrollment works without conflicts:
async function testMFAEnrollment() {
// 1. List existing factors
const { data: initialFactors } = await supabase.auth.mfa.listFactors();
console.log('Initial factors:', initialFactors.all.length);
// 2. Clean up unverified
for (const factor of initialFactors.all) {
if (factor.status === 'unverified') {
await supabase.auth.mfa.unenrollFactor(factor.id);
}
}
// 3. Enroll new factor
const { data: newFactor, error } = await supabase.auth.mfa.enrollFactor({
factorType: 'totp',
friendlyName: 'Test Authenticator',
});
if (error) {
console.error('Enrollment failed:', error);
} else {
console.log('Successfully enrolled:', newFactor.friendly_name);
}
// 4. Verify it appears in list
const { data: finalFactors } = await supabase.auth.mfa.listFactors();
const newFactorExists = finalFactors.all.some(
f => f.friendly_name === 'Test Authenticator'
);
console.log('New factor visible:', newFactorExists);
}
testMFAEnrollment().catch(console.error);Understanding MFA Factor Lifecycle
When a user initiates MFA enrollment, Supabase creates an unverified factor. The factor remains unverified until the user completes verification (entering a TOTP code or confirming a phone number). If the user closes the browser or cancels the flow before completing verification, the unverified factor remains in the database.
Unverified factors count toward uniqueness checks, which is why they can cause name conflicts. This is intentional to prevent users from accumulating abandoned factors.
Best Practices for MFA Enrollment UI
1. Always clean up first: Remove all unverified factors before starting enrollment
2. Ask for unique names: Let users choose descriptive names like "iPhone" or "Work Laptop"
3. Validate before sending: Check if the name already exists before calling enrollFactor()
4. Handle cancellation: Unenroll unverified factors if the user cancels the flow
5. Show factor list: Display all existing factors so users know what names are taken
Supabase Auth v2 Changes
Recent updates to Supabase Auth improved automatic cleanup of unverified factors. However, relying only on automatic cleanup can be problematic if network issues prevent cleanup from executing. Always implement client-side cleanup as the primary defense.
Testing with Local Supabase
When testing MFA locally, use Supabase CLI to manage factors:
# List factors for a user
supabase inspect user <user_id>
# Remove a factor directly (admin only)
supabase db --command "DELETE FROM auth.mfa_factors WHERE id = '<factor_id>'"Rate Limiting
Supabase may rate-limit rapid factor enrollments. If you retry too quickly after failures, you might hit rate limits. Implement exponential backoff:
async function enrollWithRetry(friendlyName, maxRetries = 3) {
let delay = 1000;
for (let i = 0; i < maxRetries; i++) {
try {
return await supabase.auth.mfa.enrollFactor({
factorType: 'totp',
friendlyName,
});
} catch (error) {
if (i === maxRetries - 1) throw error;
console.log(`Retry in ${delay}ms...`);
await new Promise(r => setTimeout(r, delay));
delay *= 2;
}
}
}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