Dashboard Setup
- Go to the Security page.
- In the Account MFA section, enable your desired methods (TOTP and/or Passkeys).
- (Optional) Toggle “Require at onboarding” to force MFA setup during signup.
- (Optional) Toggle “Session-based MFA” to require MFA for every new session.
Your UI SDK Implementation
- TOTP
- Passkey
- React
- React Native
- JavaScript SDK
- Flutter
- Trigger gating: Check
userWithMissingInfo?.scopeforrequiresAdditionalAuth. - Detect MFA: Use
useSyncMfaFlowto know when to prompt. - Add device: Call
addDevice()→ show QR fromuri(installqrcode). - Verify:
authenticateDevice({ code })→ fetch and show recovery codes →completeAcknowledgement(). - Refresh: Call
getUserDevices()after verification.
Copy
Ask AI
import { useMfa, useSyncMfaFlow, useDynamicContext } from "@dynamic-labs/sdk-react-core";
export function AccountTotpMfa() {
const {
addDevice,
authenticateDevice,
getUserDevices,
getRecoveryCodes,
completeAcknowledgement,
} = useMfa();
const { userWithMissingInfo } = useDynamicContext();
useSyncMfaFlow({
handler: async () => {
if (userWithMissingInfo?.scope?.includes("requiresAdditionalAuth")) {
const devices = await getUserDevices();
if (devices.length === 0) {
const { uri } = await addDevice();
// Render QR from `uri`, then prompt for OTP
return;
}
// Prompt for OTP
} else {
// MFA already satisfied; show recovery codes if needed
const codes = await getRecoveryCodes();
// Show `codes` to the user, then:
await completeAcknowledgement();
}
},
});
const verifyOtp = async (code: string) => {
await authenticateDevice({ code });
const codes = await getRecoveryCodes();
// Show `codes` to the user, then:
await completeAcknowledgement();
// Refresh device list
await getUserDevices();
};
return null;
}
- Recovery codes are single-use. Regenerate with
getRecoveryCodes(true)if needed.
- Error: “401 Unauthorized” when adding a second TOTP device — only one device is supported; delete the existing device first.
- QR code not displaying — ensure
qrcodeis installed:npm install qrcode @types/qrcode. - Recovery codes not working — each code is single-use; generate new codes if exhausted.
- Add device: Call
client.mfa.addDevice(MFADeviceType.Totp)→ show secret for manual entry. - Verify:
client.mfa.verifyDevice(code, MFADeviceType.Totp)completes setup. - Manage devices:
client.mfa.getUserDevices()lists devices. - Recovery codes:
client.mfa.getRecoveryCodes(true)regenerates backup codes.
Copy
Ask AI
import React, { useState } from 'react';
import { Alert, View, Text, TextInput, Button } from 'react-native';
import { MFADeviceType } from '@dynamic-labs/sdk-api-core';
import * as Clipboard from 'expo-clipboard';
import { useDynamic } from './path-to-your-client';
export function AccountTotpMfa() {
const client = useDynamic();
const [step, setStep] = useState<'setup' | 'verify'>('setup');
const [deviceInfo, setDeviceInfo] = useState<{
id: string;
secret: string;
} | null>(null);
const [verificationCode, setVerificationCode] = useState('');
const [loading, setLoading] = useState(false);
const handleAddDevice = async () => {
try {
setLoading(true);
const result = await client.mfa.addDevice(MFADeviceType.Totp);
setDeviceInfo(result);
setStep('verify');
} catch (error) {
console.error('Failed to add MFA device:', error);
Alert.alert('Error', 'Failed to add MFA device');
} finally {
setLoading(false);
}
};
const handleVerifyDevice = async () => {
if (!deviceInfo || !verificationCode.trim()) {
Alert.alert('Error', 'Please enter the verification code');
return;
}
try {
setLoading(true);
await client.mfa.verifyDevice(verificationCode, MFADeviceType.Totp);
Alert.alert('Success', 'MFA device added successfully');
} catch (error) {
console.error('Failed to verify MFA device:', error);
Alert.alert(
'Error',
'Failed to verify MFA device. Please check your code and try again.',
);
} finally {
setLoading(false);
}
};
if (step === 'setup') {
return (
<View>
<Text>Set up Authenticator App</Text>
<Button
title="Generate Secret"
onPress={handleAddDevice}
disabled={loading}
/>
</View>
);
}
return (
<View>
<Text>Copy Secret: {deviceInfo?.secret}</Text>
<Button
title="Copy Secret"
onPress={() => Clipboard.setStringAsync(deviceInfo!.secret)}
/>
<TextInput
placeholder="Enter 6-digit code"
value={verificationCode}
onChangeText={setVerificationCode}
keyboardType="numeric"
maxLength={6}
/>
<Button
title="Verify & Add Device"
onPress={handleVerifyDevice}
disabled={loading}
/>
</View>
);
}
- Recovery codes are single-use. Regenerate with
client.mfa.getRecoveryCodes(true)if needed.
- Error: “401 Unauthorized” when adding a second TOTP device — only one device is supported; delete the existing device first.
- Recovery codes not working — each code is single-use; generate new codes if exhausted.
- Register device:
registerTotpMfaDevice()returns a QRuriandsecret. - Authenticate:
authenticateTotpMfaDevice({ code })completes the challenge. - Manage devices:
getMfaDevices()lists devices;deleteMfaDevice()deletes. - Recovery codes:
getMfaRecoveryCodes()to display;createNewMfaRecoveryCodes()to rotate;authenticateMfaRecoveryCode({ code })to unblock login.
Copy
Ask AI
import { registerTotpMfaDevice, authenticateTotpMfaDevice, getMfaDevices } from '@dynamic-labs-sdk/client';
const register = async () => {
const { uri } = await registerTotpMfaDevice();
// Render QR code from `uri`
};
const verify = async (code) => {
await authenticateTotpMfaDevice({ code });
};
const listDevices = async () => {
const devices = await getMfaDevices();
console.log(devices);
};
Copy
Ask AI
import {
getMfaRecoveryCodes,
createNewMfaRecoveryCodes,
authenticateMfaRecoveryCode,
} from '@dynamic-labs-sdk/client';
const showCodes = async () => {
const { recoveryCodes } = await getMfaRecoveryCodes();
console.log(recoveryCodes);
};
const rotateCodes = async () => {
const { recoveryCodes } = await createNewMfaRecoveryCodes();
console.log(recoveryCodes);
};
const authWithRecovery = async (code) => {
await authenticateMfaRecoveryCode({ code });
};
Copy
Ask AI
import { authenticateTotpMfaDevice, deleteMfaDevice } from '@dynamic-labs-sdk/client';
const deleteTotpDevice = async (deviceId, code) => {
// Create a single-use MFA token using the device to be deleted
await authenticateTotpMfaDevice({
code,
createMfaTokenOptions: { singleUse: true },
});
// Use the MFA token from the client to authorize deletion
const mfaToken = dynamicClient.mfaToken;
await deleteMfaDevice({ deviceId, mfaAuthToken: mfaToken });
};
- MFA module: All TOTP operations live under
DynamicSDK.instance.mfa. - Add device: Call
DynamicSDK.instance.mfa.addDevice(type: 'totp')to start TOTP setup and get the shared secret. - Show QR / secret: Build an
otpauth://URI using the returnedsecret, your app name, and the user identifier, then render it withQrImageView(fromqr_flutter) and/or show the secret for manual entry. - Verify: Call
DynamicSDK.instance.mfa.verifyDevice(code, type: 'totp')with the 6-digit code from the authenticator app to complete setup. - Manage devices:
getUserDevices()— list all MFA devices for the userupdateUserDevice(deviceId)— update metadata for a devicedeleteUserDevice(deviceId, mfaAuthToken)— delete a device after an MFA check
- Recovery codes:
getRecoveryCodes(generateNewCodes)— get existing or regenerate codesgetNewRecoveryCodes()— fetch a new set of recovery codesisPendingRecoveryCodesAcknowledgment()+completeAcknowledgement()— handle the “I’ve seen my codes” acknowledgement
- Completion events (optional):
onMfaCompletionSuccess/onMfaCompletionFailure— subscribe to MFA completion events in your app
Copy
Ask AI
import 'package:dynamic_sdk/dynamic_sdk.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:qr_flutter/qr_flutter.dart';
class MfaAddDeviceScreen extends StatefulWidget {
const MfaAddDeviceScreen({super.key});
@override
State<MfaAddDeviceScreen> createState() => _MfaAddDeviceScreenState();
}
class _MfaAddDeviceScreenState extends State<MfaAddDeviceScreen> {
String step = 'setup'; // 'setup' or 'verify'
MfaAddDevice? deviceInfo;
final TextEditingController verificationCodeController =
TextEditingController();
bool isLoading = false;
@override
void dispose() {
verificationCodeController.dispose();
super.dispose();
}
Future<void> _handleAddDevice() async {
setState(() => isLoading = true);
try {
final result =
await DynamicSDK.instance.mfa.addDevice(type: 'totp');
setState(() {
deviceInfo = result;
step = 'verify';
});
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to add MFA device: $e'),
backgroundColor: Colors.red,
),
);
} finally {
if (mounted) {
setState(() => isLoading = false);
}
}
}
Future<void> _handleVerifyDevice() async {
if (deviceInfo == null ||
verificationCodeController.text.trim().isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter the verification code'),
backgroundColor: Colors.red,
),
);
return;
}
setState(() => isLoading = true);
try {
await DynamicSDK.instance.mfa.verifyDevice(
verificationCodeController.text.trim(),
type: 'totp',
);
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('MFA device added successfully'),
backgroundColor: Colors.green,
),
);
Navigator.of(context).pop();
} catch (e) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text(
'Failed to verify MFA device. Please check your code and try again.',
),
backgroundColor: Colors.red,
),
);
} finally {
if (mounted) {
setState(() => isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Add MFA Device'),
),
body: SafeArea(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: step == 'setup'
? _buildSetupStep()
: _buildVerifyStep(),
),
),
);
}
Widget _buildSetupStep() {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Set up Authenticator App',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: isLoading ? null : _handleAddDevice,
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Generate Secret'),
),
],
);
}
Widget _buildVerifyStep() {
if (deviceInfo == null) {
return const Text('No device info available');
}
final secret = deviceInfo!.secret;
final user = DynamicSDK.instance.auth.authenticatedUser;
final accountName = user?.email ?? user?.username ?? 'Account';
final issuer = DynamicSDK.instance.props.appName ?? 'Dynamic';
// TOTP URI used by authenticator apps
final totpUri =
'otpauth://totp/${Uri.encodeComponent(issuer)}:${Uri.encodeComponent(accountName)}'
'?secret=$secret&issuer=${Uri.encodeComponent(issuer)}';
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Text(
'Scan QR Code or Copy Secret',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Center(
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.grey),
),
child: QrImageView(
data: totpUri,
size: 200,
backgroundColor: Colors.white,
),
),
),
const SizedBox(height: 16),
SelectableText(
secret,
textAlign: TextAlign.center,
style: const TextStyle(
fontFamily: 'monospace',
fontSize: 14,
),
),
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(text: secret));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Secret copied to clipboard'),
),
);
},
child: const Text('Copy Secret'),
),
const SizedBox(height: 24),
TextField(
controller: verificationCodeController,
decoration: const InputDecoration(
labelText: 'Verification Code',
hintText: 'Enter 6-digit code from your authenticator app',
border: OutlineInputBorder(),
),
keyboardType: TextInputType.number,
maxLength: 6,
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: isLoading ? null : _handleVerifyDevice,
child: isLoading
? const SizedBox(
height: 20,
width: 20,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('Verify & Add Device'),
),
],
);
}
}
- Recovery codes are single-use. Regenerate with
getRecoveryCodes(true)if needed. - You can use
isMfaRequiredForAction('someAction')to decide when to block a sensitive action behind MFA.
- Error: “401 Unauthorized” when adding a second TOTP device — only one TOTP device is supported; delete the existing device first.
- QR code not displaying — ensure
qr_flutteris installed and that you pass a validotpauth://URI. - Recovery codes not working — each code is single-use; regenerate codes when the list is exhausted.
- React
- JavaScript SDK
- Trigger gating: Check
userWithMissingInfo?.scopeforrequiresAdditionalAuth. - Detect MFA: Use
useSyncMfaFlow. - Register or authenticate: If no passkeys →
registerPasskey(), elseauthenticatePasskeyMFA(). - Recovery: After success,
getRecoveryCodes()→ show →completeAcknowledgement(). - Refresh: Call
getPasskeys()after registration/authentication.
Copy
Ask AI
import {
useSyncMfaFlow,
useMfa,
useDynamicContext,
} from "@dynamic-labs/sdk-react-core";
import { useRegisterPasskey, useAuthenticatePasskeyMFA, useGetPasskeys } from "@dynamic-labs/sdk-react-core";
export function AccountPasskeyMfa() {
const { registerPasskey } = useRegisterPasskey();
const { authenticatePasskeyMFA } = useAuthenticatePasskeyMFA();
const { getPasskeys } = useGetPasskeys();
const { getRecoveryCodes, completeAcknowledgement } = useMfa();
const { userWithMissingInfo } = useDynamicContext();
useSyncMfaFlow({
handler: async () => {
if (userWithMissingInfo?.scope?.includes("requiresAdditionalAuth")) {
const keys = await getPasskeys();
if (keys.length === 0) {
await registerPasskey();
} else {
await authenticatePasskeyMFA();
}
// Refresh passkey list
await getPasskeys();
const codes = await getRecoveryCodes();
// Show `codes` to the user, then:
await completeAcknowledgement();
} else {
// MFA already satisfied; show recovery codes if needed
const codes = await getRecoveryCodes();
// Show `codes` to the user, then:
await completeAcknowledgement();
}
},
});
return null;
}
- Passkeys MFA is currently in closed beta.
- Requires WebAuthn support; production must be served over HTTPS.
- Error: “Passkey not supported” — check browser compatibility; requires WebAuthn and HTTPS in production.
- Recovery codes not working — each recovery code is single-use; generate new codes if exhausted.
- Register:
registerPasskey()creates and registers a passkey. - Authenticate:
authenticatePasskeyMFA()completes the MFA challenge. - Manage:
getPasskeys()lists;deletePasskey()removes. - Recovery codes:
getMfaRecoveryCodes()to display;createNewMfaRecoveryCodes()to rotate;authenticateMfaRecoveryCode({ code })to unblock login.
Copy
Ask AI
import { registerPasskey, authenticatePasskeyMFA, getPasskeys } from '@dynamic-labs-sdk/client';
const register = async () => {
await registerPasskey();
};
const mfa = async () => {
await authenticatePasskeyMFA();
};
const list = async () => {
const keys = await getPasskeys();
console.log(keys);
};
Copy
Ask AI
import {
getMfaRecoveryCodes,
createNewMfaRecoveryCodes,
authenticateMfaRecoveryCode,
} from '@dynamic-labs-sdk/client';
const showCodes = async () => {
const { recoveryCodes } = await getMfaRecoveryCodes();
console.log(recoveryCodes);
};
const rotateCodes = async () => {
const { recoveryCodes } = await createNewMfaRecoveryCodes();
console.log(recoveryCodes);
};
const authWithRecovery = async (code) => {
await authenticateMfaRecoveryCode({ code });
};
Copy
Ask AI
import { deletePasskey } from '@dynamic-labs-sdk/client';
const removePasskey = async (passkeyId) => {
await deletePasskey({ passkeyId });
};
Dynamic UI Implementation
- React
- React Native
- JavaScript SDK
With the Dynamic UI, account-based MFA prompts appear automatically during onboarding/login when additional authentication is required. To explicitly trigger in your app, use
useSyncMfaFlow to detect the requirement and usePromptMfaAuth to open the UI.Note: The Dynamic UI is method-agnostic. It automatically prompts with whichever MFA method(s) you have enabled (TOTP and/or Passkeys), so separate TOTP/Passkey tabs are not required here.Copy
Ask AI
import { useSyncMfaFlow, useDynamicContext, usePromptMfaAuth } from "@dynamic-labs/sdk-react-core";
const { userWithMissingInfo } = useDynamicContext();
const promptMfaAuth = usePromptMfaAuth();
useSyncMfaFlow({
handler: async () => {
if (userWithMissingInfo?.scope?.includes("requiresAdditionalAuth")) {
// Opens the Dynamic UI to complete account-based MFA
await promptMfaAuth(); // No MFA token needed for account-based MFA
}
},
});
- useSyncMfaFlow hook to detect the requirement and open the Dynamic UI
- usePromptMfaAuth hook to prompt the user to authenticate with MFA
The React Native SDK provides a UI method to prompt MFA authentication. Use Optional: Create MFA tokenYou can optionally request an MFA token by passing
client.ui.mfa.show() to open the MFA authentication flow.Note: The Dynamic UI is method-agnostic. It automatically prompts with whichever MFA method(s) you have enabled (TOTP only for React Native), so separate TOTP/Passkey tabs are not required here.Copy
Ask AI
import React, { useState } from 'react';
import { Alert, View, Button } from 'react-native';
import { useDynamic } from './path-to-your-client';
export function MfaAuthPrompt() {
const client = useDynamic();
const [loading, setLoading] = useState(false);
const handlePromptMfaAuth = async () => {
try {
setLoading(true);
// Opens the Dynamic UI to complete account-based MFA
await client.ui.mfa.show();
Alert.alert('Success', 'MFA authentication completed successfully');
} catch (error) {
console.error('Failed to prompt MFA auth:', error);
Alert.alert('Error', 'Failed to prompt MFA authentication');
} finally {
setLoading(false);
}
};
return (
<View>
<Button
title="Prompt MFA Authentication"
onPress={handlePromptMfaAuth}
disabled={loading}
/>
</View>
);
}
createMfaToken: true:Copy
Ask AI
const mfaToken = await client.ui.mfa.show({ createMfaToken: true });
if (mfaToken) {
// Use the MFA token for sensitive operations
console.log('MFA token:', mfaToken);
}
The JavaScript SDK does not provide any Dynamic UI. On login, check if MFA is missing and prompt the user to authenticate using a registered method.
Copy
Ask AI
import {
getMfaMethods,
isUserMissingMfaAuth,
authenticatePasskeyMFA,
authenticateTotpMfaDevice,
} from '@dynamic-labs-sdk/client';
const onLogin = async () => {
const isMissing = isUserMissingMfaAuth();
if (!isMissing) return;
const methods = await getMfaMethods();
const hasPasskeys = methods.passkeys.length > 0;
if (hasPasskeys) {
await authenticatePasskeyMFA();
return;
}
// Otherwise prompt the user for their TOTP code and authenticate
await authenticateTotpMfaDevice({ code: '123456' });
};
Programmatic MFA Modal Controls
Dynamic SDKs provide granular control over individual MFA modal screens. These methods allow you to programmatically show and hide specific MFA screens in your application.Available Modal Controls
- React Native
- React
Show and hide specific MFA screens:
Copy
Ask AI
import React from 'react';
import { View, Button } from 'react-native';
import { dynamicClient } from './path-to-your-client';
export function MfaModalControls() {
return (
<View>
{/* MFA Management Screen */}
<Button
title="Show MFA Management"
onPress={() => dynamicClient.ui.mfa.manage.show()}
/>
<Button
title="Hide MFA Management"
onPress={() => dynamicClient.ui.mfa.manage.hide()}
/>
{/* MFA Type Selection Screen */}
<Button
title="Show MFA Type Selection"
onPress={() => dynamicClient.ui.mfa.chooseType.show()}
/>
<Button
title="Hide MFA Type Selection"
onPress={() => dynamicClient.ui.mfa.chooseType.hide()}
/>
{/* QR Code Screen (for TOTP setup) */}
<Button
title="Show QR Code"
onPress={() => dynamicClient.ui.mfa.qrCode.show()}
/>
<Button
title="Hide QR Code"
onPress={() => dynamicClient.ui.mfa.qrCode.hide()}
/>
{/* OTP Verification Screen */}
<Button
title="Show OTP Verification"
onPress={() => dynamicClient.ui.mfa.otpVerification.show()}
/>
<Button
title="Hide OTP Verification"
onPress={() => dynamicClient.ui.mfa.otpVerification.hide()}
/>
{/* View Backup Codes Screen */}
<Button
title="Show Backup Codes"
onPress={() => dynamicClient.ui.mfa.viewBackupCodes.show()}
/>
<Button
title="Hide Backup Codes"
onPress={() => dynamicClient.ui.mfa.viewBackupCodes.hide()}
/>
{/* Enter Backup Codes Screen */}
<Button
title="Show Enter Backup Codes"
onPress={() => dynamicClient.ui.mfa.enterBackupCodes.show()}
/>
<Button
title="Hide Enter Backup Codes"
onPress={() => dynamicClient.ui.mfa.enterBackupCodes.hide()}
/>
</View>
);
}
For React applications, use the
useDynamicModals hook to control MFA modal visibility:Copy
Ask AI
import { useDynamicModals } from '@dynamic-labs/sdk-react-core';
export function MfaModalControls() {
const {
setShowMFAManage,
setShowMfaChooseType,
setShowMfaQRCode,
setShowOTPVerification,
setShowMfaViewBackupCodes,
setShowMfaEnterBackupCodes,
} = useDynamicModals();
return (
<div>
<button onClick={() => setShowMFAManage(true)}>
Show MFA Management
</button>
<button onClick={() => setShowMfaChooseType(true)}>
Show MFA Type Selection
</button>
<button onClick={() => setShowMfaQRCode(true)}>
Show QR Code
</button>
<button onClick={() => setShowOTPVerification(true)}>
Show OTP Verification
</button>
<button onClick={() => setShowMfaViewBackupCodes(true)}>
Show Backup Codes
</button>
<button onClick={() => setShowMfaEnterBackupCodes(true)}>
Show Enter Backup Codes
</button>
</div>
);
}