Email Magic Link with Multi-Factor Authentication

This guide shows you how to implement authentication with Email Magic Link and TOTP-based multi-factor authentication in your React application.

Overview

When a user has MFA enabled with an authenticator app (TOTP), the login flow requires the following steps:

  1. The user enters their email address to request a magic link
  2. If MFA is enabled for their account, they’re prompted to enter the 6-digit TOTP code from their authenticator app (e.g., Google Authenticator)
  3. After entering a valid TOTP code, a magic link is sent to their email
  4. The user clicks the magic link from email to complete authentication
  5. Upon successful verification, the user is authenticated and redirected to the appropriate page

This two-factor approach provides an additional layer of security beyond a standard magic link.

Implementation

Step 1: Initialize Authentication and Handle MFA Required Error

First, attempt to authenticate with email. If MFA is required, an error will be thrown. You can handle this error by prompting the user to enter their TOTP code.

1import React from "react";
2import { useAuthenticate } from "@account-kit/react";
3import { MfaRequiredError } from "@account-kit/signer";
4import { useState } from "react";
5
6function MagicLinkWithMFA() {
7 const { authenticate } = useAuthenticate();
8
9 // Step 1: Handle initial email submission and check for MFA requirement
10 const handleInitialAuthentication = (email: string) => {
11 authenticate(
12 {
13 type: "email",
14 emailMode: "magicLink",
15 email,
16 },
17 {
18 onSuccess: () => {
19 // This callback only fires when the entire auth flow is complete
20 // (user clicked magic link and completed MFA if required)
21 console.log("Authentication successful!");
22 },
23 onError: (error) => {
24 // If MFA is required the attempt will result in an MfaRequiredError
25 if (error instanceof MfaRequiredError) {
26 const { multiFactorId } = error.multiFactors[0];
27
28 // Store the multiFactorId to use when the user enters their TOTP code
29
30 // In step 2, we will prompt the user to enter their TOTP code (from their authenticator app)
31 // and we'll use this multiFactorId to verify the TOTP code
32 }
33 // Handle other errors
34 },
35 }
36 );
37 };
38
39 return <div>{/* Your UI components here */}</div>;
40}

Once we have the MFA data from the first step, we can complete the authentication by submitting the TOTP code with the multiFactorId. You must prompt the user to enter their TOTP code (from their authenticator app) and then submit it with the multiFactorId.

1import React from "react";
2import { useAuthenticate } from "@account-kit/react";
3
4// Continuing from the previous component...
5
6function MagicLinkWithMFA() {
7 const { authenticate } = useAuthenticate();
8
9 // Prompt the user to enter their TOTP code (from their authenticator app)
10 // Hardcoded for now, but in a real app you'd get this from the user
11 const totpCode = "123456";
12 const multiFactorId = "123456"; // This is the multiFactorId from the first step
13
14 // Step 2: Submit the TOTP code with multiFactorId to complete the flow
15 const handleMfaSubmission = (email: string) => {
16 authenticate(
17 {
18 type: "email",
19 emailMode: "magicLink",
20 email,
21 // The multiFactors array tells the authentication system which
22 // factor to verify and what code to use
23 multiFactors: [
24 {
25 multiFactorId,
26 multiFactorCode: totpCode,
27 },
28 ],
29 },
30 {
31 onSuccess: () => {
32 // This callback will only fire after the user has clicked the magic link and the email has been verified
33 },
34 onError: (error) => {
35 // Handle error
36 },
37 }
38 );
39 };
40
41 return <div>{/* Your UI components here */}</div>;
42}

When the user clicks the magic link in their email, your application needs to handle the redirect and complete the authentication. The magic link will redirect to your application with a bundle parameter. You must submit this bundle to the authenticate function to complete the authentication.

1import React, { useEffect } from "react";
2import { useAuthenticate } from "@account-kit/react";
3
4function MagicLinkRedirect() {
5 const { authenticate } = useAuthenticate();
6
7 const handleMagicLinkRedirect = () => {
8 const url = new URL(window.location.href);
9 const bundle = url.searchParams.get("bundle");
10
11 // If there's no bundle parameter, this isn't a magic link redirect
12 if (!bundle) return;
13
14 authenticate(
15 {
16 type: "email",
17 bundle,
18 },
19 {
20 onSuccess: () => {
21 // onSuccess only fires once the entire flow is done (email magic link + optional MFA).
22 // It still runs even if the final step completes in another tab/window.
23 },
24 onError: (error) => {
25 // Handle error
26 },
27 }
28 );
29 };
30
31 // Call this function when the component mounts
32 useEffect(() => {
33 handleMagicLinkRedirect();
34 }, []);
35}

Next Steps