Skip to main content

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:
  1. Enrollment: Users scan a QR code or manually enter a secret in their authenticator app
  2. Setup verification: Users confirm setup by entering a 6-digit code from their app
  3. 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

  • TypeScript
  • JavaScript
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

  • React
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

  • TypeScript
  • JavaScript
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

  • React
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:
  • TypeScript
  • React
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

  • 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

  1. Onboarding timing: Don’t force MFA during initial signup - let users explore first
  2. Code input: Use numeric input fields with automatic focus advancement
  3. Error handling: Provide specific error messages (expired code vs. wrong code)
  4. 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
Recovery options:
  • Implement account recovery through primary authentication method
  • Allow MFA reset after email/SMS verification