Multi-Factor Authentication (MFA)

Alchemy Signer supports Time-based One-Time Passwords (TOTP) multi-factor authentication (MFA). This lets you prompt users to set up a TOTP authenticator (e.g. Google Authenticator) as an additional security factor.

Multi-factor authentication is currently supported when authenticating with Email OTP or Email Magic-link

Setting up Multi-Factor Authentication

1. Add a new TOTP factor

Once the user is authenticated, you can call addMfa to enable TOTP. This returns factor details including an ID and setup information that your app can display to the user (e.g. a QR code or otpauth link that the user can scan in Google Authenticator).

1import { signer } from "./signer";
2
3const { multiFactors } = await signer.addMFA({
4 multiFactorType: "totp",
5});
6
7// Display the QR code or secret to the user
8const totpUrl = result?.multiFactors[0].multiFactorTotpUrl;
9const multiFactorId = result?.multiFactors[0].multiFactorId;

You can show the multiFactorTotpUrl in your UI as a QR code or link for the user to add it to their authenticator app.

2. Verify the TOTP setup

Once the user has scanned the TOTP secret, have them enter the 6-digit code from their authenticator app. Then call verifyMfa:

1import { signer } from "./signer";
2
3await signer.verifyMfa({
4 multiFactorId, // from addMfa
5 multiFactorCode: "123456",
6});

3. Remove a TOTP factor

If a user wants to disable TOTP, call removeMfa with the multiFactorId you want to remove:

1import { signer } from "./signer";
2
3await signer.removeMfa({
4 multiFactorIds: [multiFactorId],
5});

4. Get a list of existing MFA factors

1import { signer } from "./signer";
2
3const { multiFactors } = await signer.getMfaFactors();

Authenticating Email OTP with multi-factor TOTP

Step 1: Send an OTP to user’s email

1import { signer } from "./signer";
2
3signer.authenticate({
4 type: "email",
5 emailMode: "otp",
6 email: "[email protected]",
7});

Step 2: Submit the email OTP code

1import { signer } from "./signer";
2
3signer.authenticate({
4 type: "otp",
5 otpCode: "EMAIL_OTP_CODE",
6});

Step 3: Submit the TOTP code (authenticator app code)

1import { signer } from "./signer";
2
3const user = await signer?.validateMultiFactors({
4 multiFactorCode: totpCode,
5});

When calling authenticate with emailMode="magicLink", you can catch a MfaRequiredError. Then you can collect the TOTP code and resubmit.

1import { MfaRequiredError } from "@account-kit/signer";
2import { signer } from "./signer";
3
4const promptUserForCode = async () => {
5 // Prompt user for TOTP code
6 // const totpCode = await promptUserForCode();
7
8 return "123456";
9};
10
11try {
12 // Promise resolves when the user is fully authenticated (email magic link + optional MFA),
13 // even if completion happens in another tab/window
14 await signer.authenticate({
15 type: "email",
16 email: "[email protected]",
17 emailMode: "magicLink",
18 });
19} catch (err) {
20 if (err instanceof MfaRequiredError) {
21 // Prompt user for TOTP code
22 const totpCode = await promptUserForCode();
23
24 const { multiFactorId } = err.multiFactors[0];
25
26 // Promise resolves when the user is fully authenticated (email magic link + optional MFA),
27 // even if completion happens in another tab/window
28 await signer.authenticate({
29 type: "email",
30 emailMode: "magicLink",
31 email: "[email protected]",
32 multiFactors: [
33 {
34 multiFactorId,
35 multiFactorCode: totpCode,
36 },
37 ],
38 });
39 } else {
40 // handle other errors
41 }
42}

Authenticating Social Login with multi-factor TOTP

When a user has MFA enabled using an authenticator app, the authentication process for social login is seamless. Unlike email authentication flows, you don’t need to handle the MFA challenge manually in your code.

The TOTP verification happens automatically during the OAuth callback flow:

  1. The user authenticates with the social provider (Google, Facebook, etc.)
  2. After successful provider authentication, they’re prompted for their TOTP code on the OAuth callback page
  3. Once verified, authentication completes normally

Simply use the standard social login authentication as shown in the Social Login Authentication guide:

1import { signer } from "./signer";
2
3await signer.authenticate({
4 type: "oauth",
5 authProviderId: "google", // Choose between the auth providers you selected to support from your auth policy
6 mode: "redirect", // Alternatively, you can choose "popup" mode
7 redirectUrl: "/", // After logging in, redirect to the index page
8});