Supabase returns this error when a user attempts to access a protected resource without meeting the required Multi-Factor Authentication (MFA) level. Authenticator Assurance Level (AAL) is a security concept that defines how strongly a user has authenticated, and certain operations require higher assurance levels than basic password authentication.
Supabase implements Authenticator Assurance Levels (AAL) as part of its Multi-Factor Authentication (MFA) system. AAL1 represents single-factor authentication (password only), while AAL2 requires multi-factor authentication (password + second factor like TOTP or SMS). When you configure certain operations or endpoints to require AAL2, Supabase will reject requests from users who have only authenticated with AAL1. This error occurs when a user tries to perform a sensitive operation (like changing account settings, accessing financial data, or modifying security configurations) without having completed MFA setup or verification.
First, verify whether the user has MFA configured. You can check this through the Supabase dashboard or by querying the user's MFA factors:
import { createClient } from '@supabase/supabase-js'
const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY)
// Get current session
const { data: { session } } = await supabase.auth.getSession()
// Check MFA factors for the user
const { data: factors } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
console.log('Current AAL:', factors.currentLevel)
console.log('Next required AAL:', factors.nextLevel)If the user doesn't have MFA enabled, they'll need to set it up before accessing AAL2-protected resources.
If the user needs to enable MFA, guide them through the setup process:
// Start MFA enrollment
const { data, error } = await supabase.auth.mfa.enroll({
factorType: 'totp', // or 'sms'
})
// For TOTP, show QR code to user
if (data?.totp?.qr_code) {
// Display QR code for user to scan with authenticator app
console.log('QR Code URL:', data.totp.qr_code)
}
// After user scans QR code and enters code:
const { error: verifyError } = await supabase.auth.mfa.verify({
factorId: data.id,
code: userEnteredCode,
})
// For SMS MFA:
const { data: smsData } = await supabase.auth.mfa.enroll({
factorType: 'sms',
phone: userPhoneNumber,
})
// User will receive SMS with code to verifyOnce MFA is set up, the user will need to authenticate with both password and MFA to achieve AAL2.
Update your application logic to check AAL levels and prompt for MFA when needed:
// Before performing sensitive operations, check AAL
const { data: aalData } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
if (aalData.currentLevel !== aalData.nextLevel) {
// User needs to elevate authentication level
// Redirect to MFA verification page
window.location.href = '/auth/mfa-verify'
} else {
// User has required AAL, proceed with operation
await performSensitiveOperation()
}
// After MFA verification, retry the original request
const { error } = await performSensitiveOperation()
if (!error) {
// Operation succeeded with elevated AAL
}Consider implementing middleware or hooks that automatically check AAL requirements for protected routes.
If you're using Row Level Security (RLS), check if your policies require AAL2:
-- Example RLS policy that requires AAL2
CREATE POLICY "Require MFA for sensitive data" ON sensitive_table
FOR ALL USING (
auth.jwt() ->> 'aal' = 'aal2'
AND -- other conditions
);
-- Check current policies
SELECT * FROM pg_policies
WHERE tablename = 'your_table';You may need to adjust policies to:
1. Allow AAL1 for non-sensitive operations
2. Require AAL2 only for specific sensitive operations
3. Provide clear error messages when AAL requirements aren't met
Also check if you have any Edge Functions or custom business logic that checks AAL levels.
Provide clear feedback to users when they encounter AAL errors:
try {
await performSensitiveOperation()
} catch (error) {
if (error.message.includes('insufficient_aal')) {
// Show user-friendly message
showNotification({
title: 'Additional Verification Required',
message: 'This action requires multi-factor authentication. Please set up or verify MFA.',
action: {
label: 'Set up MFA',
onClick: () => navigateToMfaSetup()
}
})
// Optionally, store the intended action to retry after MFA
localStorage.setItem('pending_action', JSON.stringify({
action: 'sensitive_operation',
params: operationParams
}))
}
}Make sure your UI clearly indicates which operations require MFA and provide easy paths for users to set it up.
Supabase's AAL implementation follows the IETF's Authentication Context Class Reference (ACR) and Authenticator Assurance Level (AAL) concepts from standards like OpenID Connect. AAL1 corresponds to single-factor authentication, while AAL2 requires multi-factor authentication. When designing your security model, consider implementing step-up authentication: allow users to access most features with AAL1, but require AAL2 for sensitive operations. This balances security with user experience. Remember that MFA sessions have their own expiration times, and users may need to re-authenticate with MFA periodically for continued access to AAL2-protected resources.
email_conflict_identity_not_deletable: Cannot delete identity because of email conflict
How to fix "Cannot delete identity because of email conflict" in Supabase
mfa_challenge_expired: MFA challenge has expired
How to fix "mfa_challenge_expired: MFA challenge has expired" in Supabase
conflict: Database conflict, usually related to concurrent requests
How to fix "database conflict usually related to concurrent requests" in Supabase
phone_exists: Phone number already exists
How to fix "phone_exists" in Supabase
StorageApiError: resource_already_exists
StorageApiError: Resource already exists