NERO
Guides

Add Multi-Factor Authentication

Protect sensitive operations with TOTP authenticator codes

Add Multi-Factor Authentication

Add TOTP-based MFA to protect sensitive operations like signing, key rotation, and recovery. Users scan a QR code with any authenticator app (Google Authenticator, Authy, 1Password) and enter 6-digit codes to authorize protected actions.

Prerequisites

  • @nerochain/mpc-sdk installed with an authenticated user session
  • qrcode.react for rendering the TOTP QR code
npm install qrcode.react

The MFA Flow

  1. Check status -- query whether MFA is already enabled
  2. Setup TOTP -- generate a TOTP secret and display it as a QR code
  3. Verify setup -- user scans the QR code and enters a 6-digit code to confirm
  4. Save backup codes -- store the one-time backup codes for emergency access
  5. Create challenges -- require MFA verification before protected operations
  6. Verify challenges -- user provides a TOTP code to authorize the operation

Step 1 -- Check MFA Status

Call getStatus() to see if the user has MFA enabled and which methods are configured.

import { useNeroMFA } from "@nerochain/mpc-sdk/react";

function MfaStatus() {
  const { getStatus, isLoading } = useNeroMFA();
  const [status, setStatus] = useState(null);

  const checkStatus = async () => {
    const s = await getStatus();
    setStatus(s);
  };

  return (
    <div>
      <button onClick={checkStatus} disabled={isLoading}>
        Check MFA Status
      </button>
      {status && <pre>{JSON.stringify(status, null, 2)}</pre>}
    </div>
  );
}

The response includes whether MFA is enabled, which methods are active, and when they were configured.

Step 2 -- Set Up TOTP

Call setupTotp() to generate a new TOTP secret. The SDK returns an otpauthUrl (the standard URI format that authenticator apps understand) and a methodId that identifies this TOTP method.

const { setupTotp } = useNeroMFA();

const handleSetup = async () => {
  const { methodId, otpauthUrl, backupCodes } = await setupTotp();
  // methodId: unique ID for this TOTP method -- save for verification
  // otpauthUrl: "otpauth://totp/NERO:user@example.com?secret=BASE32SECRET&issuer=NERO"
  // backupCodes: one-time recovery codes
};

Step 3 -- Display the QR Code

Render the otpauthUrl as a QR code using qrcode.react. The user scans this with their authenticator app to register the TOTP secret.

import { QRCodeSVG } from "qrcode.react";

function TotpQrCode({ otpauthUrl }: { otpauthUrl: string }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
      <QRCodeSVG value={otpauthUrl} size={200} />
      <p>Scan with your authenticator app, then enter the code below.</p>
    </div>
  );
}

Step 4 -- Verify the Setup

After the user scans the QR code, they enter a 6-digit code from their authenticator app. Call verifyTotpSetup() with the methodId from step 2 and the code to confirm the setup.

const { verifyTotpSetup } = useNeroMFA();

const handleVerify = async (methodId: string, code: string) => {
  const result = await verifyTotpSetup(methodId, code);
  // MFA is now active for this account
};

Step 5 -- Save Backup Codes

The setupTotp() response includes one-time backup codes. Display these to the user and instruct them to save them in a secure location. Each backup code can be used exactly once as a substitute for a TOTP code.

function BackupCodes({ codes }: { codes: string[] }) {
  return (
    <div>
      <h3>Backup Codes</h3>
      <p>Save these codes in a secure location. Each code can only be used once.</p>
      <ul>
        {codes.map((code) => (
          <li key={code}>
            <code>{code}</code>
          </li>
        ))}
      </ul>
      <button onClick={() => navigator.clipboard.writeText(codes.join("\n"))}>
        Copy All Codes
      </button>
    </div>
  );
}

Step 6 -- Create and Verify MFA Challenges

Once MFA is enabled, protect sensitive operations by creating a challenge. The user must verify the challenge with a TOTP code (or backup code) before the operation proceeds.

const { createChallenge, verifyChallenge } = useNeroMFA();

const handleProtectedOperation = async (operation: string, totpCode: string) => {
  // 1. Create a challenge for the operation
  const { challengeId } = await createChallenge(operation);

  // 2. Verify the challenge with a TOTP code
  const result = await verifyChallenge(challengeId, { code: totpCode });
  // Operation is now authorized
};

Supported operation types:

OperationDescription
signingStandard transaction or message signing
high_value_signingTransactions above a configured threshold
recovery_initiationStarting the wallet recovery flow
device_addRegistering a new trusted device
mfa_disableDisabling MFA protection
key_rotationRotating the MPC key shares

Complete Working Component

import { useState } from "react";
import { useNeroMFA } from "@nerochain/mpc-sdk/react";
import { QRCodeSVG } from "qrcode.react";

type Step = "status" | "setup" | "verify" | "done" | "challenge";

export default function MfaSetupGuide() {
  const {
    getStatus,
    setupTotp,
    verifyTotpSetup,
    createChallenge,
    verifyChallenge,
    isLoading,
    error,
  } = useNeroMFA();

  const [step, setStep] = useState<Step>("status");
  const [methodId, setMethodId] = useState("");
  const [otpauthUrl, setOtpauthUrl] = useState("");
  const [backupCodes, setBackupCodes] = useState<string[]>([]);
  const [totpCode, setTotpCode] = useState("");
  const [challengeId, setChallengeId] = useState("");
  const [challengeCode, setChallengeCode] = useState("");
  const [result, setResult] = useState<unknown>(null);

  const handleCheckStatus = async () => {
    try {
      const s = await getStatus();
      setResult(s);
    } catch {
      // error state handled by hook
    }
  };

  const handleSetupTotp = async () => {
    try {
      const res = await setupTotp();
      setMethodId(res.methodId);
      setOtpauthUrl(res.otpauthUrl);
      if (res.backupCodes) setBackupCodes(res.backupCodes);
      setStep("setup");
    } catch {
      // error state handled by hook
    }
  };

  const handleVerifySetup = async () => {
    try {
      await verifyTotpSetup(methodId, totpCode);
      setStep("done");
    } catch {
      // error state handled by hook
    }
  };

  const handleCreateChallenge = async () => {
    try {
      const res = await createChallenge("signing");
      setChallengeId(res.challengeId);
      setStep("challenge");
    } catch {
      // error state handled by hook
    }
  };

  const handleVerifyChallenge = async () => {
    try {
      const res = await verifyChallenge(challengeId, { code: challengeCode });
      setResult(res);
    } catch {
      // error state handled by hook
    }
  };

  return (
    <div style={{ maxWidth: 480, margin: "0 auto" }}>
      <h2>MFA Setup</h2>

      {error && <p style={{ color: "red" }}>{error.message}</p>}

      {step === "status" && (
        <div>
          <button onClick={handleCheckStatus} disabled={isLoading}>
            Check MFA Status
          </button>
          <button onClick={handleSetupTotp} disabled={isLoading}>
            Enable TOTP
          </button>
          {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
        </div>
      )}

      {step === "setup" && otpauthUrl && (
        <div>
          <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8, padding: 16 }}>
            <QRCodeSVG value={otpauthUrl} size={200} />
            <p>Scan this QR code with your authenticator app.</p>
          </div>

          {backupCodes.length > 0 && (
            <div style={{ padding: 12, background: "#fef3c7", borderRadius: 8, marginBottom: 12 }}>
              <strong>Save these backup codes:</strong>
              <ul>
                {backupCodes.map((code) => (
                  <li key={code}><code>{code}</code></li>
                ))}
              </ul>
            </div>
          )}

          <input
            type="text"
            value={totpCode}
            onChange={(e) => setTotpCode(e.target.value)}
            placeholder="6-digit code from authenticator"
            maxLength={6}
          />
          <button onClick={handleVerifySetup} disabled={isLoading || totpCode.length !== 6}>
            Verify Setup
          </button>
        </div>
      )}

      {step === "done" && (
        <div>
          <p>MFA is enabled. Protected operations now require a TOTP code.</p>
          <button onClick={handleCreateChallenge} disabled={isLoading}>
            Test: Create a Signing Challenge
          </button>
        </div>
      )}

      {step === "challenge" && (
        <div>
          <p>Challenge created. Enter your TOTP code to authorize.</p>
          <input
            type="text"
            value={challengeCode}
            onChange={(e) => setChallengeCode(e.target.value)}
            placeholder="6-digit code"
            maxLength={6}
          />
          <button onClick={handleVerifyChallenge} disabled={isLoading || challengeCode.length !== 6}>
            Verify Challenge
          </button>
          {result && <pre>{JSON.stringify(result, null, 2)}</pre>}
        </div>
      )}
    </div>
  );
}

What the User Sees

  1. Check status -- a JSON response showing whether MFA is enabled
  2. Enable TOTP -- a QR code appears on screen, plus a list of backup codes
  3. Scan + verify -- they open Google Authenticator, scan the code, and type in the 6-digit number
  4. Confirmation -- "MFA is enabled" message with option to test a challenge
  5. Challenge flow -- they enter a TOTP code to authorize a signing operation

Error Handling

try {
  await verifyTotpSetup(methodId, code);
} catch (err) {
  if (err.message.includes("invalid code")) {
    // Wrong TOTP code -- ask user to try again
  } else if (err.message.includes("expired")) {
    // TOTP setup session expired -- call setupTotp() again
  }
}
ErrorCauseFix
Invalid codeWrong TOTP code enteredAsk the user to wait for the next code cycle and try again
Setup expiredToo much time between setupTotp() and verifyTotpSetup()Call setupTotp() again to get a fresh QR code
Challenge expiredChallenge not verified in timeCreate a new challenge with createChallenge()
MFA already enabledTOTP already configured for this accountCall getStatus() to check current configuration

On this page