Multiple MFA Methods
Privy allows users to register multiple MFA methods (e.g. SMS, TOTP, and passkeys) if they wish.
If a user already has one MFA method enabled, and wishes to enroll in another (or change their
existing MFA method), Privy will require the user to complete MFA with their existing method in
order to complete enrollment of the new, additional method.
When a user has multiple MFA methods enrolled, they will be given a choice of which method to use during verification:
Accessing enrolled methods
You can check which MFA methods are available for the current user through the user
object:
const {user} = usePrivy();
console.log(user.mfaMethods);
// ['sms', 'totp', 'passkey'] for a user who has enrolled in all three methods
This can be useful for determining which options to present to users in your custom UIs.
Unenrolling MFA Methods
Default UI approach
When using Privy’s default UIs, users can remove MFA methods directly through the MFA management modal. Simply call the same method used for enrollment:
const {showMfaEnrollmentModal} = useMfaEnrollment();
// The same modal allows users to add or remove MFA methods
<button onClick={showMfaEnrollmentModal}>Manage MFA Methods</button>
const {showMfaEnrollmentModal} = useMfaEnrollment();
// The same modal allows users to add or remove MFA methods
<button onClick={showMfaEnrollmentModal}>Manage MFA Methods</button>
const {init: initMfaEnrollmentUI} = useMfaEnrollmentUI();
const onManageMfa = async () => {
await initMfaEnrollmentUI({mfaMethods: ['sms', 'totp', 'passkey']});
};
<Button onPress={onManageMfa}>Manage MFA Methods</Button>
Custom UI approach
For custom UIs, Privy provides methods to unenroll specific MFA methods:
import {useMfaEnrollment} from '@privy-io/react-auth';
export default function MfaUnenrollment() {
const {unenrollWithSms, unenrollWithTotp, unenrollWithPasskey} = useMfaEnrollment();
return (
<div>
<button onClick={unenrollWithSms}>Unenroll SMS</button>
<button onClick={unenrollWithTotp}>Unenroll TOTP</button>
<button onClick={unenrollWithPasskey}>Unenroll passkey</button>
</div>
);
}
import {useMfaEnrollment} from '@privy-io/react-auth';
export default function MfaUnenrollment() {
const {unenrollWithSms, unenrollWithTotp, unenrollWithPasskey} = useMfaEnrollment();
return (
<div>
<button onClick={unenrollWithSms}>Unenroll SMS</button>
<button onClick={unenrollWithTotp}>Unenroll TOTP</button>
<button onClick={unenrollWithPasskey}>Unenroll passkey</button>
</div>
);
}
import {useMfaEnrollment} from '@privy-io/expo';
export default function MfaUnenrollment() {
const {unenrollMfa} = useMfaEnrollment();
return (
<YStack>
<Button onClick={() => unenrollMfa({method: 'sms'})}>
<Text>Unenroll SMS</Text>
</Button>
<Button onClick={() => unenrollMfa({method: 'totp'})}>
<Text>Unenroll TOTP</Text>
</Button>
<Button onClick={() => unenrollMfa({method: 'passkey'})}>
<Text>Unenroll passkey</Text>
</Button>
</YStack>
);
}
When invoked, unenrollment methods will require the user to first complete MFA verification to confirm unenrollment. This ensures that only the authorized user can remove security methods.
Special considerations for passkeys
By default, unenrolling a passkey as an MFA method will also unlink it as a valid login method. You can modify this behavior to keep the passkey as a login method:
Configure in the PrivyProvider
<PrivyProvider
appId="your-privy-app-id"
config={{
...theRestOfYourConfig,
passkeys: {
// Set this to `false` to keep the passkey as a login method after unenrolling from MFA
shouldUnlinkOnUnenrollMfa: false
}
}}
>
{/* your app's content */}
</PrivyProvider>
Configure in the PrivyProvider
<PrivyProvider
appId="your-privy-app-id"
config={{
...theRestOfYourConfig,
passkeys: {
// Set this to `false` to keep the passkey as a login method after unenrolling from MFA
shouldUnlinkOnUnenrollMfa: false
}
}}
>
{/* your app's content */}
</PrivyProvider>
Option 1: Configure in the PrivyElements component
<PrivyElements config={{passkeys: {shouldUnlinkOnUnenrollMfa: false}}} />
// Option 2: Use the removeForLogin parameter directly in the unenrollMfa call
<Button onClick={() => unenrollMfa({method: 'passkey', removeForLogin: false})}>
<Text>Unenroll passkey, keep as login method</Text>
</Button>
Error Handling for MFA Codes
When users are submitting MFA codes (both during enrollment and verification), several types of errors can occur:
- Incorrect code: The user entered the wrong MFA code
- Maximum attempts reached: The user has tried too many incorrect codes
- Timeout: The MFA code has expired and a new one must be requested
Helper functions for error types
Privy provides helper functions to identify the type of error:
import {
errorIndicatesMfaVerificationFailed,
errorIndicatesMfaMaxAttempts,
errorIndicatesMfaTimeout
} from '@privy-io/react-auth'; // or '@privy-io/expo'
try {
await submit({method: 'sms', mfaCode: userEnteredCode});
} catch (e) {
if (errorIndicatesMfaVerificationFailed(e)) {
// Wrong code - allow the user to try again
setError('Incorrect code. Please try again.');
} else if (errorIndicatesMfaMaxAttempts(e)) {
// Too many attempts - request a new code
setError('Too many failed attempts. Please request a new code.');
resetMfaFlow();
} else if (errorIndicatesMfaTimeout(e)) {
// Code expired - request a new code
setError('Your code has expired. Please request a new code.');
resetMfaFlow();
} else {
// Some other error occurred
setError('An unexpected error occurred. Please try again later.');
console.error(e);
}
}
Best practices for handling errors
- Clear error messaging: Provide specific feedback based on the error type
- Reset flow after max attempts: After the maximum number of attempts, clear state and allow the user to request a new code
- Timeouts: Inform users when codes expire and provide a way to request new ones
- Exponential backoff: Consider implementing a slight delay after multiple failures to discourage brute force attempts
Optimistic MFA Verification
For an optimal user experience, there are cases where you may want to prompt for MFA verification before it’s automatically required by the system. This is called “optimistic verification.”
An example use case: A user with TOTP enabled wants to add SMS as a second MFA method. Instead of
waiting for the system to prompt for TOTP verification during the SMS enrollment, you can
proactively ask for TOTP verification upfront for a smoother user flow.
Using promptMfa for optimistic verification
const {promptMfa} = useMfa();
const {initEnrollmentWithSms} = useMfaEnrollment();
const handleAddSmsMethod = async () => {
// Proactively verify with existing MFA method first
await promptMfa();
// After successful verification, proceed with new method enrollment
const phoneNumber = await getUserPhoneInput();
await initEnrollmentWithSms({phoneNumber});
// Continue with SMS enrollment flow...
};
const {promptMfa} = useMfa();
const {initEnrollmentWithSms} = useMfaEnrollment();
const handleAddSmsMethod = async () => {
// Proactively verify with existing MFA method first
await promptMfa();
// After successful verification, proceed with new method enrollment
const phoneNumber = await getUserPhoneInput();
await initEnrollmentWithSms({phoneNumber});
// Continue with SMS enrollment flow...
};
const {promptMfa} = useMfa();
const {initMfaEnrollment} = useMfaEnrollment();
const handleAddSmsMethod = async () => {
// Proactively verify with existing MFA method first
await promptMfa();
// After successful verification, proceed with new method enrollment
const phoneNumber = await getUserPhoneInput();
await initMfaEnrollment({method: 'sms', phoneNumber});
// Continue with SMS enrollment flow...
};
The promptMfa
method will trigger the MFA verification flow only if necessary (if the user doesn’t already have an active MFA token). If verification is successful, a token is cached for 15 minutes, and subsequent operations won’t require additional MFA verification.
Benefits of optimistic verification
- Improved UX: Consolidate authentication steps at logical points in the user journey
- Reduced friction: Avoid interrupting users in the middle of a multi-step process
- Predictable flow: Create a more controlled and predictable verification experience
Keep in mind that optimistic verification is entirely optional - the wallet will always require MFA when needed. This approach simply gives you more control over when and how the verification occurs.