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-sdkinstalled with an authenticated user sessionqrcode.reactfor rendering the TOTP QR code
npm install qrcode.reactThe MFA Flow
- Check status -- query whether MFA is already enabled
- Setup TOTP -- generate a TOTP secret and display it as a QR code
- Verify setup -- user scans the QR code and enters a 6-digit code to confirm
- Save backup codes -- store the one-time backup codes for emergency access
- Create challenges -- require MFA verification before protected operations
- 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:
| Operation | Description |
|---|---|
signing | Standard transaction or message signing |
high_value_signing | Transactions above a configured threshold |
recovery_initiation | Starting the wallet recovery flow |
device_add | Registering a new trusted device |
mfa_disable | Disabling MFA protection |
key_rotation | Rotating 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
- Check status -- a JSON response showing whether MFA is enabled
- Enable TOTP -- a QR code appears on screen, plus a list of backup codes
- Scan + verify -- they open Google Authenticator, scan the code, and type in the 6-digit number
- Confirmation -- "MFA is enabled" message with option to test a challenge
- 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
}
}| Error | Cause | Fix |
|---|---|---|
| Invalid code | Wrong TOTP code entered | Ask the user to wait for the next code cycle and try again |
| Setup expired | Too much time between setupTotp() and verifyTotpSetup() | Call setupTotp() again to get a fresh QR code |
| Challenge expired | Challenge not verified in time | Create a new challenge with createChallenge() |
| MFA already enabled | TOTP already configured for this account | Call getStatus() to check current configuration |