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:
- The user enters their email address to request a magic link
- 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)
- After entering a valid TOTP code, a magic link is sent to their email
- The user clicks the magic link from email to complete authentication
- 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.
import React from "react";
import { useAuthenticate } from "@account-kit/react";
import { MfaRequiredError } from "@account-kit/signer";
import { useState } from "react";
function MagicLinkWithMFA() {
const { authenticate } = useAuthenticate();
// Step 1: Handle initial email submission and check for MFA requirement
const handleInitialAuthentication = (email: string) => {
authenticate(
{
type: "email",
emailMode: "magicLink",
email,
},
{
onSuccess: () => {
// This callback only fires when the entire auth flow is complete
// (user clicked magic link and completed MFA if required)
console.log("Authentication successful!");
},
onError: (error) => {
// If MFA is required the attempt will result in an MfaRequiredError
if (error instanceof MfaRequiredError) {
const { multiFactorId } = error.multiFactors[0];
// Store the multiFactorId to use when the user enters their TOTP code
// In step 2, we will prompt the user to enter their TOTP code (from their authenticator app)
// and we'll use this multiFactorId to verify the TOTP code
}
// Handle other errors
},
}
);
};
return <div>{/* Your UI components here */}</div>;
}
Step 2: Submit TOTP Code and Complete Magic Link Authentication
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.
import React from "react";
import { useAuthenticate } from "@account-kit/react";
// Continuing from the previous component...
function MagicLinkWithMFA() {
const { authenticate } = useAuthenticate();
// Prompt the user to enter their TOTP code (from their authenticator app)
// Hardcoded for now, but in a real app you'd get this from the user
const totpCode = "123456";
const multiFactorId = "123456"; // This is the multiFactorId from the first step
// Step 2: Submit the TOTP code with multiFactorId to complete the flow
const handleMfaSubmission = (email: string) => {
authenticate(
{
type: "email",
emailMode: "magicLink",
email,
// The multiFactors array tells the authentication system which
// factor to verify and what code to use
multiFactors: [
{
multiFactorId,
multiFactorCode: totpCode,
},
],
},
{
onSuccess: () => {
// This callback will only fire after the user has clicked the magic link and the email has been verified
},
onError: (error) => {
// Handle error
},
}
);
};
return <div>{/* Your UI components here */}</div>;
}
Step 3: Handle the Magic Link Redirect
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.
import React, { useEffect } from "react";
import { useAuthenticate } from "@account-kit/react";
function MagicLinkRedirect() {
const { authenticate } = useAuthenticate();
const handleMagicLinkRedirect = () => {
const url = new URL(window.location.href);
const bundle = url.searchParams.get("bundle");
// If there's no bundle parameter, this isn't a magic link redirect
if (!bundle) return;
authenticate(
{
type: "email",
bundle,
},
{
onSuccess: () => {
// Authentication successful!
},
onError: (error) => {
// Handle error
},
}
);
};
// Call this function when the component mounts
useEffect(() => {
handleMagicLinkRedirect();
}, []);
}