Email OTP with Multi-Factor Authentication

This guide shows you how to implement Email OTP authentication when a user has multi-factor authentication (MFA) enabled.

Overview

When MFA is enabled, the authentication process requires two steps:

  1. Verify the user’s email with a one-time password
  2. Verify the 6-digit code (TOTP) from their authenticator app

Implementation

Step 1: Start Email OTP Authentication

First, initiate the email OTP authentication process:

import React from "react";
import { 
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react";
// Inside your component const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
();
const
const handleSendCode: (email: string) => void
handleSendCode
= (
email: string
email
: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
(
{
type: "email"
type
: "email",
emailMode?: "otp" | "magicLink" | undefined
emailMode
: "otp",
email: string
email
,
}, {
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefined
onSuccess
: () => {
// This callback will only fire after both email OTP and MFA (if required) are completed },
MutateOptions<User, Error, AuthParams, unknown>.onError?: ((error: Error, variables: AuthParams, context: unknown) => void) | undefined
onError
: (
error: Error
error
) => {
// Handle error
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const code = 5; console.error('error #%d', code); // Prints: error #5, to stderr console.error('error', code); // Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

error
(
error: Error
error
);
}, }, ); };

Step 2: Submit the OTP Code

After the user receives the email OTP, they must submit the code to continue.

The signer status will change to AWAITING_EMAIL_AUTH when an OTP code needs to be submitted:

import { 
const useSignerStatus: (override?: AlchemyAccountContextProps) => UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
,
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
} from "@account-kit/react";
import {
enum AlchemySignerStatus
AlchemySignerStatus
} from "@account-kit/signer";
import React, {
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void

Accepts a function that contains imperative, possibly effectful code.

useEffect
} from "react";
function
function EmailOtpVerification(): JSX.Element
EmailOtpVerification
() {
const {
const status: AlchemySignerStatus
status
} =
function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
();
const {
const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>
authenticate
,
const isPending: boolean
isPending
} =
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
({
onError?: ((error: Error, variables: AuthParams, context: unknown) => Promise<unknown> | unknown) | undefined
onError
: (
error: Error
error
) => {
// Handle OTP verification errors
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const code = 5; console.error('error #%d', code); // Prints: error #5, to stderr console.error('error', code); // Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

error
("OTP verification failed:",
error: Error
error
);
}, }); // Called when user enters their OTP code from email const
const handleVerify: (emailOtp: string) => void
handleVerify
= (
emailOtp: string
emailOtp
: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => void
authenticate
({
type: "otp"
type
: "otp",
otpCode: string
otpCode
:
emailOtp: string
emailOtp
,
}); }; // Example of prompting user when OTP verification is needed
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void

Accepts a function that contains imperative, possibly effectful code.

useEffect
(() => {
if (
const status: AlchemySignerStatus
status
===
enum AlchemySignerStatus
AlchemySignerStatus
.
function (enum member) AlchemySignerStatus.AWAITING_EMAIL_AUTH = "AWAITING_EMAIL_AUTH"
AWAITING_EMAIL_AUTH
) {
// Show OTP input UI to the user } }, [
const status: AlchemySignerStatus
status
]);
return ( // Your OTP input UI <
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>{/* OTP input component */}</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
); }

Step 3: Complete Authentication

If MFA is required, the signer status will change to AWAITING_MFA_AUTH. You’ll need to collect and submit the TOTP code from the user’s authenticator app:

import {
  
const useSignerStatus: (override?: AlchemyAccountContextProps) => UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
,
const useSigner: <T extends AlchemySigner>() => T | null

Hook for accessing the current Alchemy signer within a React component. It uses a synchronous external store for updates. This is a good use case if you want to use the signer as an EOA, giving you direct access to it. The signer returned from useSigner just does a personal_sign or eth_signTypedData without any additional logic, but a smart contract account might have additional logic for creating signatures for 1271 validation so useSignMessage or useSignTypeData instead.

useSigner
,
function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResult

Hook that provides functions and state for authenticating a user using a signer. It includes methods for both synchronous and asynchronous mutations. Useful if building your own UI components and want to control the authentication flow. For authenticate vs authenticateAsync, use authenticate when you want the hook the handle state changes for you, authenticateAsync when you need to wait for the result to finish processing.

This can be complex for magic link or OTP flows: OPT calls authenticate twice, but this should be handled by the signer.

useAuthenticate
,
} from "@account-kit/react"; import {
enum AlchemySignerStatus
AlchemySignerStatus
} from "@account-kit/signer";
import React, {
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void

Accepts a function that contains imperative, possibly effectful code.

useEffect
,
function useState<S>(initialState: S | (() => S)): [S, React.Dispatch<React.SetStateAction<S>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
} from "react";
function
function MfaVerification(): JSX.Element
MfaVerification
() {
const
const signer: AlchemySigner | null
signer
=
useSigner<AlchemySigner>(): AlchemySigner | null

Hook for accessing the current Alchemy signer within a React component. It uses a synchronous external store for updates. This is a good use case if you want to use the signer as an EOA, giving you direct access to it. The signer returned from useSigner just does a personal_sign or eth_signTypedData without any additional logic, but a smart contract account might have additional logic for creating signatures for 1271 validation so useSignMessage or useSignTypeData instead.

useSigner
();
const {
const status: AlchemySignerStatus
status
} =
function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResult

Hook to get the signer status, optionally using an override configuration, useful if you’re building your own login.

useSignerStatus
();
const [
const isVerifying: boolean
isVerifying
,
const setIsVerifying: React.Dispatch<React.SetStateAction<boolean>>
setIsVerifying
] =
useState<boolean>(initialState: boolean | (() => boolean)): [boolean, React.Dispatch<React.SetStateAction<boolean>>] (+1 overload)

Returns a stateful value, and a function to update it.

useState
(false);
// Called when user enters their TOTP code from authenticator app const
const handleVerify: (totpCode: string) => Promise<void>
handleVerify
= async (
totpCode: string
totpCode
: string) => {
try {
const setIsVerifying: (value: React.SetStateAction<boolean>) => void
setIsVerifying
(true);
await
const signer: AlchemySigner | null
signer
?.
BaseAlchemySigner<TClient extends BaseSignerClient>.validateMultiFactors(params: ValidateMultiFactorsArgs): Promise<User>

Validates MFA factors that were required during authentication. This function should be called after MFA is required and the user has provided their MFA code. It completes the authentication process by validating the MFA factors and completing the auth bundle.

validateMultiFactors
({
multiFactorCode: string
multiFactorCode
:
totpCode: string
totpCode
,
}); // After successful MFA validation, the user will be authenticated // and the onSuccess callback from the initial authenticate call will fire } catch (
function (local var) error: unknown
error
) {
var console: Console

The console module provides a simple debugging console that is similar to the JavaScript console mechanism provided by web browsers.

The module exports two specific components:

  • A Console class with methods such as console.log(), console.error() and console.warn() that can be used to write to any Node.js stream. * A global console instance configured to write to process.stdout and process.stderr. The global console can be used without importing the node:console module.

Warning: The global console object's methods are neither consistently synchronous like the browser APIs they resemble, nor are they consistently asynchronous like all other Node.js streams. See the note on process I/O for more information.

Example using the global console:


const name = 'Will Robinson'; console.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to stderr ```

Example using the `Console` class:

```js const out = getStreamSomehow(); const err = getStreamSomehow(); const myConsole = new console.Console(out, err);

myConsole.log('hello world'); // Prints: hello world, to out myConsole.log('hello %s', 'world'); // Prints: hello world, to out myConsole.error(new Error('Whoops, something bad happened')); // Prints: [Error: Whoops, something bad happened], to err

const name = 'Will Robinson'; myConsole.warn(`Danger $name! Danger!`); // Prints: Danger Will Robinson! Danger!, to err ```
console
.
Console.error(message?: any, ...optionalParams: any[]): void (+1 overload)

Prints to stderr with newline. Multiple arguments can be passed, with the first used as the primary message and all additional used as substitution values similar to printf(3) (the arguments are all passed to util.format()).

js const code = 5; console.error('error #%d', code); // Prints: error #5, to stderr console.error('error', code); // Prints: error 5, to stderr

If formatting elements (e.g. %d) are not found in the first string then util.inspect() is called on each argument and the resulting string values are concatenated. See util.format() for more information.

error
("MFA verification failed:",
function (local var) error: unknown
error
);
} finally {
const setIsVerifying: (value: React.SetStateAction<boolean>) => void
setIsVerifying
(false);
} }; // Example of prompting user when MFA verification is needed
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): void

Accepts a function that contains imperative, possibly effectful code.

useEffect
(() => {
if (
const status: AlchemySignerStatus
status
===
enum AlchemySignerStatus
AlchemySignerStatus
.
function (enum member) AlchemySignerStatus.AWAITING_MFA_AUTH = "AWAITING_MFA_AUTH"
AWAITING_MFA_AUTH
) {
// Show TOTP input UI to the user } }, [
const status: AlchemySignerStatus
status
]);
return ( // Your TOTP input UI <
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>{/* TOTP input component */}</
React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
div
>
); }

Next Steps