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).

import { 
import signer
signer
} from "./signer";
const {
const multiFactors: any
multiFactors
} = await
import signer
signer
.
any
addMFA
({
multiFactorType: string
multiFactorType
: "totp",
}); // Display the QR code or secret to the user const
const totpUrl: any
totpUrl
=
any
result
?.
any
multiFactors
[0].
any
multiFactorTotpUrl
;
const
const multiFactorId: any
multiFactorId
=
any
result
?.
any
multiFactors
[0].
any
multiFactorId
;
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

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:

import { 
import signer
signer
} from "./signer";
await
import signer
signer
.
any
verifyMfa
({
multiFactorId: any
multiFactorId
, // from addMfa
multiFactorCode: string
multiFactorCode
: "123456",
});
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

3. Remove a TOTP factor

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

import { 
import signer
signer
} from "./signer";
await
import signer
signer
.
any
removeMfa
({
multiFactorIds: any[]
multiFactorIds
: [
any
multiFactorId
],
});
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

4. Get a list of existing MFA factors

import { 
import signer
signer
} from "./signer";
const {
const multiFactors: any
multiFactors
} = await
import signer
signer
.
any
getMfaFactors
();
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

Authenticating Email OTP with multi-factor TOTP

Step 1: Send an OTP to user’s email

import { 
import signer
signer
} from "./signer";
import signer
signer
.
any
authenticate
({
type: string
type
: "email",
emailMode: string
emailMode
: "otp",
email: string
email
: "[email protected]",
});
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

Step 2: Submit the email OTP code

import { 
import signer
signer
} from "./signer";
import signer
signer
.
any
authenticate
({
type: string
type
: "otp",
otpCode: string
otpCode
: "EMAIL_OTP_CODE",
});
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

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

import { 
import signer
signer
} from "./signer";
const
const user: any
user
= await
import signer
signer
?.
any
validateMultiFactors
({
multiFactorCode: any
multiFactorCode
:
any
totpCode
,
});
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

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

import { 
class MfaRequiredError
MfaRequiredError
} from "@account-kit/signer";
import {
import signer
signer
} from "./signer";
const
const promptUserForCode: () => Promise<string>
promptUserForCode
= async () => {
// Prompt user for TOTP code // const totpCode = await promptUserForCode(); return "123456"; }; try { // Promise resolves when the user is fully authenticated (email magic link + optional MFA), // even if completion happens in another tab/window await
import signer
signer
.
any
authenticate
({
type: string
type
: "email",
email: string
email
: "[email protected]",
emailMode: string
emailMode
: "magicLink",
}); } catch (
var err: unknown
err
) {
if (
var err: unknown
err
instanceof
class MfaRequiredError
MfaRequiredError
) {
// Prompt user for TOTP code const
const totpCode: string
totpCode
= await
const promptUserForCode: () => Promise<string>
promptUserForCode
();
const {
const multiFactorId: string
multiFactorId
} =
var err: MfaRequiredError
err
.
MfaRequiredError.multiFactors: MfaFactor[]
multiFactors
[0];
// Promise resolves when the user is fully authenticated (email magic link + optional MFA), // even if completion happens in another tab/window await
import signer
signer
.
any
authenticate
({
type: string
type
: "email",
emailMode: string
emailMode
: "magicLink",
email: string
email
: "[email protected]",
multiFactors: { multiFactorId: string; multiFactorCode: string; }[]
multiFactors
: [
{
multiFactorId: string
multiFactorId
,
multiFactorCode: string
multiFactorCode
:
const totpCode: string
totpCode
,
}, ], }); } else { // handle other errors } }
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });

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:

import { 
import signer
signer
} from "./signer";
await
import signer
signer
.
any
authenticate
({
type: string
type
: "oauth",
authProviderId: string
authProviderId
: "google", // Choose between the auth providers you selected to support from your auth policy
mode: string
mode
: "redirect", // Alternatively, you can choose "popup" mode
redirectUrl: string
redirectUrl
: "/", // After logging in, redirect to the index page
});
import { 
class AlchemyWebSigner

A SmartAccountSigner that can be used with any SmartContractAccount

AlchemyWebSigner
} from "@account-kit/signer";
export const
const signer: AlchemyWebSigner
signer
= new
new AlchemyWebSigner(params: AlchemySignerParams): AlchemyWebSigner

Initializes an instance with the provided Alchemy signer parameters after parsing them with a schema.

AlchemyWebSigner
({
client: ({ connection: { apiKey: string; rpcUrl?: undefined; jwt?: undefined; } | { jwt: string; rpcUrl?: undefined; apiKey?: undefined; } | { rpcUrl: string; apiKey?: undefined; jwt?: undefined; } | { rpcUrl: string; jwt: string; apiKey?: undefined; }; ... 4 more ...; enablePopupOauth?: boolean | undefined; } | AlchemySignerWebClient) & (AlchemySignerWebClient | ... 1 more ... | undefined)
client
: {
connection: { apiKey: string; }
connection
: {
apiKey: string
apiKey
: "API_KEY",
},
iframeConfig: { iframeContainerId: string; }
iframeConfig
: {
iframeContainerId: string
iframeContainerId
: "alchemy-signer-iframe-container",
}, }, });