Overview
Multi-Factor Authentication (MFA) adds an extra layer of security to Embedded Wallets by requiring users to verify their identity through a secondary authentication method. The SDK supports Time-based One-Time Password (TOTP) authentication using authenticator apps like Google Authenticator, Authy, or 1Password.
MFA is optional but strongly recommended for production applications handling significant value or sensitive operations. It provides defense against account takeover attacks even if the primary authentication method is compromised.
How MFA works
Embedded Wallets use TOTP (Time-based One-Time Password) for multi-factor authentication:
Enrollment : Users scan a QR code or manually enter a secret in their authenticator app
Setup verification : Users confirm setup by entering a 6-digit code from their app
Future authentication : Users provide a 6-digit code for sensitive operations
Important : Users must be authenticated (signed in) before they can enroll in MFA or perform MFA verification. MFA is an additional security layer on top of existing authentication methods.
MFA enrollment
The enrollment process establishes the shared secret between the user’s authenticator app and your application. This is a one-time setup per user.
Core SDK implementation
import {
initiateMfaEnrollment ,
submitMfaEnrollment ,
getCurrentUser
} from "@coinbase/cdp-core" ;
async function enrollUserInMfa () {
// Step 1: Initiate MFA enrollment (user must be signed in)
const enrollment = await initiateMfaEnrollment ({
mfaMethod: "totp"
});
// Display QR code for user to scan with their authenticator app
console . log ( "Scan this QR code URL:" , enrollment . authUrl );
// Or display the secret for manual entry
console . log ( "Or enter this secret manually:" , enrollment . secret );
// Step 2: After user adds to their authenticator app, verify with the 6-digit code
const result = await submitMfaEnrollment ({
mfaMethod: "totp" ,
mfaCode: "123456" // The 6-digit code from the user's authenticator app
});
// After successful enrollment, the user object is updated with MFA information
console . log ( "MFA enrolled for user:" , result . user . userId );
console . log ( "MFA enrollment info:" , result . user . mfaMethods ?. totp );
// Output: { enrolledAt: "2024-01-01T00:00:00Z" }
// The current user now has MFA enabled
const user = await getCurrentUser ();
console . log ( "User MFA status:" , user . mfaMethods );
}
React implementation
import {
useInitiateMfaEnrollment ,
useSubmitMfaEnrollment ,
useCurrentUser
} from "@coinbase/cdp-react" ;
import { useState } from "react" ;
import QRCode from "react-qr-code" ;
function MfaEnrollment () {
const { initiateMfaEnrollment } = useInitiateMfaEnrollment ();
const { submitMfaEnrollment } = useSubmitMfaEnrollment ();
const { currentUser } = useCurrentUser ();
const [ enrollmentData , setEnrollmentData ] = useState ( null );
const [ mfaCode , setMfaCode ] = useState ( "" );
const [ isLoading , setIsLoading ] = useState ( false );
const [ error , setError ] = useState ( null );
const handleInitiateEnrollment = async () => {
setIsLoading ( true );
setError ( null );
try {
// Step 1: Initiate MFA enrollment
const result = await initiateMfaEnrollment ({ mfaMethod: "totp" });
setEnrollmentData ( result );
} catch ( error ) {
setError ( "Failed to initiate MFA enrollment" );
console . error ( error );
} finally {
setIsLoading ( false );
}
};
const handleSubmitEnrollment = async () => {
setIsLoading ( true );
setError ( null );
try {
// Step 2: Submit the 6-digit code from authenticator app
const result = await submitMfaEnrollment ({
mfaMethod: "totp" ,
mfaCode: mfaCode ,
});
console . log ( "MFA enrolled successfully" );
setEnrollmentData ( null );
setMfaCode ( "" );
} catch ( error ) {
setError ( "Invalid code. Please try again." );
console . error ( error );
} finally {
setIsLoading ( false );
}
};
if ( ! currentUser ) {
return < div > Please sign in to enable MFA </ div > ;
}
if ( currentUser . mfaMethods ?. totp ) {
return < div > MFA is already enabled for your account </ div > ;
}
return (
< div className = "mfa-enrollment" >
< h2 > Enable Two-Factor Authentication </ h2 >
{ error && < div className = "error" > { error } </ div > }
{ ! enrollmentData ? (
< div >
< p > Secure your account with an authenticator app </ p >
< button
onClick = { handleInitiateEnrollment }
disabled = { isLoading }
>
{ isLoading ? "Loading..." : "Set Up MFA" }
</ button >
</ div >
) : (
< div className = "enrollment-steps" >
< h3 > Step 1: Scan QR Code </ h3 >
< p > Use your authenticator app to scan this QR code: </ p >
< div className = "qr-container" >
< QRCode value = { enrollmentData . authUrl } size = { 200 } />
</ div >
< details >
< summary > Can't scan? Enter manually </ summary >
< p > Secret key: < code > { enrollmentData . secret } </ code ></ p >
</ details >
< h3 > Step 2: Enter Verification Code </ h3 >
< p > Enter the 6-digit code from your authenticator app: </ p >
< input
type = "text"
placeholder = "000000"
value = { mfaCode }
onChange = { ( e ) => setMfaCode ( e . target . value . replace ( / \D / g , '' ). slice ( 0 , 6 )) }
maxLength = { 6 }
pattern = "[0-9]{6}"
inputMode = "numeric"
autoComplete = "one-time-code"
/>
< button
onClick = { handleSubmitEnrollment }
disabled = { mfaCode . length !== 6 || isLoading }
>
{ isLoading ? "Verifying..." : "Verify and Enable MFA" }
</ button >
</ div >
) }
</ div >
);
}
MFA verification
Once enrolled, users must provide MFA verification for sensitive operations or when explicitly required by your application.
Core SDK implementation
import {
initiateMfaVerification ,
submitMfaVerification
} from "@coinbase/cdp-core" ;
async function performSensitiveOperation () {
try {
// Attempt the sensitive operation
await signEvmTransaction ({ /* transaction details */ });
} catch ( error ) {
// If MFA is required, the operation will fail with an error
// Step 1: Initiate MFA verification
await initiateMfaVerification ({
mfaMethod: "totp"
});
// Step 2: Get MFA code from user and submit
const mfaCode = await getUserInput ( "Enter your 6-digit MFA code:" );
await submitMfaVerification ({
mfaMethod: "totp" ,
mfaCode: mfaCode
});
// Step 3: Retry the operation after successful MFA verification
await signEvmTransaction ({ /* transaction details */ });
}
}
React implementation
import {
useInitiateMfaVerification ,
useSubmitMfaVerification ,
useSignEvmTransaction
} from "@coinbase/cdp-react" ;
import { useState } from "react" ;
function SensitiveOperation () {
const { initiateMfaVerification } = useInitiateMfaVerification ();
const { submitMfaVerification } = useSubmitMfaVerification ();
const { signEvmTransaction } = useSignEvmTransaction ();
const [ mfaCode , setMfaCode ] = useState ( "" );
const [ requiresMfa , setRequiresMfa ] = useState ( false );
const [ isLoading , setIsLoading ] = useState ( false );
const [ error , setError ] = useState ( null );
const handleOperation = async () => {
setIsLoading ( true );
setError ( null );
try {
// Attempt the operation
await signEvmTransaction ({ /* transaction details */ });
console . log ( "Operation completed successfully" );
} catch ( error ) {
// Check if MFA is required
if ( error . code === 'MFA_REQUIRED' ) {
// Initiate MFA verification
await initiateMfaVerification ({ mfaMethod: "totp" });
setRequiresMfa ( true );
} else {
setError ( "Operation failed" );
console . error ( error );
}
} finally {
setIsLoading ( false );
}
};
const handleMfaSubmit = async () => {
setIsLoading ( true );
setError ( null );
try {
// Submit MFA code
await submitMfaVerification ({
mfaMethod: "totp" ,
mfaCode: mfaCode ,
});
// Retry the operation
await signEvmTransaction ({ /* transaction details */ });
console . log ( "Operation completed with MFA" );
setRequiresMfa ( false );
setMfaCode ( "" );
} catch ( error ) {
setError ( "Invalid MFA code. Please try again." );
console . error ( error );
} finally {
setIsLoading ( false );
}
};
return (
< div className = "sensitive-operation" >
{ error && < div className = "error" > { error } </ div > }
{ ! requiresMfa ? (
< button
onClick = { handleOperation }
disabled = { isLoading }
>
{ isLoading ? "Processing..." : "Perform Sensitive Operation" }
</ button >
) : (
< div className = "mfa-verification" >
< h3 > MFA Verification Required </ h3 >
< p > Enter your 6-digit code from your authenticator app: </ p >
< input
type = "text"
placeholder = "000000"
value = { mfaCode }
onChange = { ( e ) => setMfaCode ( e . target . value . replace ( / \D / g , '' ). slice ( 0 , 6 )) }
maxLength = { 6 }
pattern = "[0-9]{6}"
inputMode = "numeric"
autoComplete = "one-time-code"
/>
< button
onClick = { handleMfaSubmit }
disabled = { mfaCode . length !== 6 || isLoading }
>
{ isLoading ? "Verifying..." : "Verify and Continue" }
</ button >
</ div >
) }
</ div >
);
}
Checking MFA status
You can check if a user has MFA enabled by examining their user object:
import { getCurrentUser } from "@coinbase/cdp-core" ;
async function checkMfaStatus () {
const user = await getCurrentUser ();
if ( user . mfaMethods ?. totp ) {
console . log ( "MFA is enabled" );
console . log ( "Enrolled at:" , user . mfaMethods . totp . enrolledAt );
} else {
console . log ( "MFA is not enabled" );
}
}
Best practices
Security recommendations
Enforce MFA for high-value operations
Require MFA verification for transactions above certain thresholds
Mandate MFA for account recovery or security setting changes
Consider requiring MFA for wallet exports or private key access
Allow users to re-enroll if they lose access to their authenticator app
Consider implementing account recovery flows for MFA-locked accounts
Explain what MFA is and why it’s important
Provide setup guides for popular authenticator apps
Show clear error messages when MFA codes are invalid or expired
Start with optional MFA to build user trust
Gradually encourage adoption through incentives or reminders
Make MFA mandatory only when necessary for compliance or security
UX considerations
Onboarding timing : Don’t force MFA during initial signup - let users explore first
Code input : Use numeric input fields with automatic focus advancement
Error handling : Provide specific error messages (expired code vs. wrong code)
Recovery options : Always provide a way for users to disable MFA if needed
Supported authenticator apps
The following authenticator apps are tested and fully supported:
Google Authenticator
Microsoft Authenticator
Authy
1Password
LastPass Authenticator
Duo Mobile
Any TOTP-compatible authenticator app will work. The apps listed above are commonly used and well-tested with our implementation.
Complete example
Here’s a full implementation showing the complete MFA flow from enrollment to verification:
import {
initialize ,
signInWithEmail ,
verifyEmailOTP ,
initiateMfaEnrollment ,
submitMfaEnrollment ,
initiateMfaVerification ,
submitMfaVerification ,
getCurrentUser ,
signEvmTransaction
} from "@coinbase/cdp-core" ;
async function completeMfaFlow () {
// Initialize the SDK
await initialize ({
projectId: "your-project-id"
});
// Sign in the user
const { flowId } = await signInWithEmail ({
email: "[email protected] "
});
const { user } = await verifyEmailOTP ({
flowId ,
otp: "123456"
});
// Check if MFA is already enabled
if ( ! user . mfaMethods ?. totp ) {
// Enroll in MFA
const enrollment = await initiateMfaEnrollment ({
mfaMethod: "totp"
});
// In a real app, display QR code using a library like qrcode or react-qr-code
console . log ( "QR Code URL:" , enrollment . authUrl );
console . log ( "Manual entry secret:" , enrollment . secret );
// Get the verification code from user input
const enrollmentCode = "654321" ; // From user's authenticator app
// Complete enrollment
const result = await submitMfaEnrollment ({
mfaMethod: "totp" ,
mfaCode: enrollmentCode
});
console . log ( "MFA successfully enabled!" );
}
// Later, when performing a sensitive operation...
try {
// Attempt the operation
const signature = await signEvmTransaction ({
from: "0x..." ,
to: "0x..." ,
value: "0x..." ,
data: "0x..."
});
} catch ( error ) {
// If MFA is required
if ( error . code === 'MFA_REQUIRED' ) {
// Initiate MFA verification
await initiateMfaVerification ({
mfaMethod: "totp"
});
// Get current MFA code from user
const verificationCode = "123789" ; // From user's authenticator app
// Submit verification
await submitMfaVerification ({
mfaMethod: "totp" ,
mfaCode: verificationCode
});
// Retry the operation
const signature = await signEvmTransaction ({
from: "0x..." ,
to: "0x..." ,
value: "0x..." ,
data: "0x..."
});
console . log ( "Transaction signed with MFA:" , signature );
}
}
}
Troubleshooting
Common causes:
Time synchronization issues between device and server
User entering an expired code (codes refresh every 30 seconds)
Incorrect authenticator app setup
Solutions:
Ensure device time is synchronized
Ask users to wait for a new code and try again
Provide option to re-enroll in MFA
Lost authenticator app access
Recovery options:
Implement account recovery through primary authentication method
Allow MFA reset after email/SMS verification
What to read next