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:
- Verify the user’s email with a one-time password
- 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): UseAuthenticateResultHook 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): UseAuthenticateResultHook 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) => voidhandleSendCode = (email: stringemail: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate(
{
type: "email"type: "email",
emailMode?: "otp" | "magicLink" | undefinedemailMode: "otp",
email: stringemail,
},
{
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefinedonSuccess: () => {
// 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) | undefinedonError: (error: Errorerror) => {
// Handle error
var console: ConsoleThe 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: Errorerror);
},
},
);
};
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) => UseSignerStatusResultHook to get the signer status, optionally using an override configuration, useful if you’re building your own login.
useSignerStatus, function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook 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 AlchemySignerStatusAlchemySignerStatus } from "@account-kit/signer";
import React, { function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts a function that contains imperative, possibly effectful code.
useEffect } from "react";
function function EmailOtpVerification(): JSX.ElementEmailOtpVerification() {
const { const status: AlchemySignerStatusstatus } = function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResultHook 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: booleanisPending } = function useAuthenticate(mutationArgs?: UseAuthenticateMutationArgs): UseAuthenticateResultHook 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) | undefinedonError: (error: Errorerror) => {
// Handle OTP verification errors
var console: ConsoleThe 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: Errorerror);
},
});
// Called when user enters their OTP code from email
const const handleVerify: (emailOtp: string) => voidhandleVerify = (emailOtp: stringemailOtp: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate({
type: "otp"type: "otp",
otpCode: stringotpCode: emailOtp: stringemailOtp,
});
};
// Example of prompting user when OTP verification is needed
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts a function that contains imperative, possibly effectful code.
useEffect(() => {
if (const status: AlchemySignerStatusstatus === enum AlchemySignerStatusAlchemySignerStatus.function (enum member) AlchemySignerStatus.AWAITING_EMAIL_AUTH = "AWAITING_EMAIL_AUTH"AWAITING_EMAIL_AUTH) {
// Show OTP input UI to the user
}
}, [const status: AlchemySignerStatusstatus]);
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) => UseSignerStatusResultHook 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 | nullHook 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): UseAuthenticateResultHook 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 AlchemySignerStatusAlchemySignerStatus } from "@account-kit/signer";
import React, { function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts 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.ElementMfaVerification() {
const const signer: AlchemySigner | nullsigner = useSigner<AlchemySigner>(): AlchemySigner | nullHook 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: AlchemySignerStatusstatus } = function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResultHook to get the signer status, optionally using an override configuration, useful if you’re building your own login.
useSignerStatus();
const [const isVerifying: booleanisVerifying, 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: stringtotpCode: string) => {
try {
const setIsVerifying: (value: React.SetStateAction<boolean>) => voidsetIsVerifying(true);
await const signer: AlchemySigner | nullsigner?.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: stringmultiFactorCode: totpCode: stringtotpCode,
});
// 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: unknownerror) {
var console: ConsoleThe 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: unknownerror);
} finally {
const setIsVerifying: (value: React.SetStateAction<boolean>) => voidsetIsVerifying(false);
}
};
// Example of prompting user when MFA verification is needed
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts a function that contains imperative, possibly effectful code.
useEffect(() => {
if (const status: AlchemySignerStatusstatus === enum AlchemySignerStatusAlchemySignerStatus.function (enum member) AlchemySignerStatus.AWAITING_MFA_AUTH = "AWAITING_MFA_AUTH"AWAITING_MFA_AUTH) {
// Show TOTP input UI to the user
}
}, [const status: AlchemySignerStatusstatus]);
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>
);
}