Email Magic Link Authentication
Email magic link authentication is a two-step process:
- The user enters their email address and requests a magic link
- The user clicks the link in their email, which redirects them back to your application to complete authentication
Email OTP has been shown to have up to a 3x higher conversion rate and a 10-second faster flow compared to magic links. Consider using Email OTP for better user experience.
You can implement Email Magic Link authentication in two ways:
- Pre-built UI Components - Quick implementation with minimal code
- Custom UI - Complete control over the user experience
Pre-built UI Components
Account Kit provides pre-built UI components that handle the entire Email Magic Link authentication flow with minimal code.
Step 1: Add Authentication Components to Your Page
Before configuring your authentication, first add one of the pre-built components to your application:
Using Modal Authentication
To add authentication in a modal popup:
import React from "react";
import { const useAuthModal: () => {
isOpen: boolean;
openAuthModal: () => void;
closeAuthModal: () => void;
}A hook that returns the open and close functions for the Auth Modal if uiConfig is enabled on the Account Provider
useAuthModal } from "@account-kit/react";
export default function function MyPage(): JSX.ElementMyPage() {
const { const openAuthModal: () => voidopenAuthModal } = function useAuthModal(): {
isOpen: boolean;
openAuthModal: () => void;
closeAuthModal: () => void;
}A hook that returns the open and close functions for the Auth Modal if uiConfig is enabled on the Account Provider
useAuthModal();
return <React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button React.DOMAttributes<HTMLButtonElement>.onClick?: React.MouseEventHandler<HTMLButtonElement> | undefinedonClick={const openAuthModal: () => voidopenAuthModal}>Sign in</React.JSX.IntrinsicElements.button: React.DetailedHTMLProps<React.ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>button>;
}
For more details on modal configuration, see the Modal Authentication documentation.
Or:
Using Embedded Authentication
To embed authentication directly in your page:
import React from "react";
import { const AuthCard: (props: AuthCardProps) => JSX.ElementReact component containing an Auth view with configured auth methods and options based on the config passed to the AlchemyAccountProvider
AuthCard } from "@account-kit/react";
export default function function MyLoginPage(): JSX.ElementMyLoginPage() {
return (
<React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div React.HTMLAttributes<HTMLDivElement>.className?: string | undefinedclassName="flex flex-row p-4 bg-white border border-gray-200 rounded-lg">
<const AuthCard: (props: AuthCardProps) => JSX.ElementReact component containing an Auth view with configured auth methods and options based on the config passed to the AlchemyAccountProvider
AuthCard />
</React.JSX.IntrinsicElements.div: React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>div>
);
}
For more details on embedded authentication, see the Embedded Authentication documentation.
Step 2: Configure Email Magic Link in UI Components
After adding the components, configure the Email Magic Link authentication in your application config:
import { type AlchemyAccountsUIConfig = {
auth?: {
addPasskeyOnSignup?: boolean;
header?: React.ReactNode;
hideError?: boolean;
onAuthSuccess?: () => void;
sections: AuthType[][];
hideSignInText?: boolean;
};
illustrationStyle?: "outline" | "linear" | "filled" | "flat" | undefined;
modalBaseClassName?: string;
supportUrl?: string | undefined;
}AlchemyAccountsUIConfig, const createConfig: (props: CreateConfigProps, ui?: AlchemyAccountsUIConfig) => AlchemyAccountsConfigWithUIWraps the createConfig
that is exported from @aa-sdk/core
to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).
createConfig } from "@account-kit/react";
import { const sepolia: Chainsepolia, function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy } from "@account-kit/infra";
const const uiConfig: AlchemyAccountsUIConfiguiConfig: type AlchemyAccountsUIConfig = {
auth?: {
addPasskeyOnSignup?: boolean;
header?: React.ReactNode;
hideError?: boolean;
onAuthSuccess?: () => void;
sections: AuthType[][];
hideSignInText?: boolean;
};
illustrationStyle?: "outline" | "linear" | "filled" | "flat" | undefined;
modalBaseClassName?: string;
supportUrl?: string | undefined;
}AlchemyAccountsUIConfig = {
auth?: {
addPasskeyOnSignup?: boolean;
header?: React.ReactNode;
hideError?: boolean;
onAuthSuccess?: () => void;
sections: AuthType[][];
hideSignInText?: boolean;
} | undefinedauth: {
sections: AuthType[][]Each section can contain multiple auth types which will be grouped together and separated by an OR divider
sections: [
[
{
type: "email"type: "email",
emailMode?: "magicLink" | "otp" | undefinedemailMode: "magicLink",
// Optional customizations:
buttonLabel?: string | undefinedbuttonLabel: "Continue with Email",
placeholder?: string | undefinedplaceholder: "Enter your email address",
},
],
],
},
};
export const const config: AlchemyAccountsConfigWithUIconfig = function createConfig(props: CreateConfigProps, ui?: AlchemyAccountsUIConfig): AlchemyAccountsConfigWithUIWraps the createConfig
that is exported from @aa-sdk/core
to allow passing an additional argument, the configuration object for the Auth Components UI (the modal and AuthCard).
createConfig(
{
transport: AlchemyTransporttransport: function alchemy(config: AlchemyTransportConfig): AlchemyTransportCreates an Alchemy transport with the specified configuration options. When sending all traffic to Alchemy, you must pass in one of rpcUrl, apiKey, or jwt. If you want to send Bundler and Paymaster traffic to Alchemy and Node traffic to a different RPC, you must pass in alchemyConnection and nodeRpcUrl.
alchemy({ apiKey: stringapiKey: "your-api-key" }),
chain: Chainchain: const sepolia: Chainsepolia,
},
const uiConfig: AlchemyAccountsUIConfiguiConfig,
);
Email Magic Link configuration accepts the following options:
You can find the full type definition in the Account Kit source code.
For more details on UI component customization, see the UI Components documentation.
Custom UI
If you need complete control over the user experience, you can implement your own custom UI for Email Magic Link authentication using Account Kit hooks.
Step 1: Send the Magic Link
First, prompt your user for their email address and send a magic link:
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();
// When the user submits their email
const const handleSendMagicLink: (email: string) => voidhandleSendMagicLink = (email: stringemail: string) => {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate(
{
type: "email"type: "email",
emailMode?: "otp" | "magicLink" | undefinedemailMode: "magicLink",
email: stringemail,
},
{
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefinedonSuccess: () => {
// onSuccess only fires once the entire flow is done (email magic link + optional MFA).
// It still runs even if the final step completes in another tab/window.
},
MutateOptions<User, Error, AuthParams, unknown>.onError?: ((error: Error, variables: AuthParams, context: unknown) => void) | undefinedonError: (error: Errorerror) => {
// Handle error
},
},
);
};
Step 2: Handle the Redirect
When the user clicks the magic link in their email, they’ll be redirected back to your application. You need to extract the authentication bundle from the URL and complete the authentication:
import { function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts a function that contains imperative, possibly effectful code.
useEffect } 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();
// Handle the redirect when the component mounts
function useEffect(effect: React.EffectCallback, deps?: React.DependencyList): voidAccepts a function that contains imperative, possibly effectful code.
useEffect(() => {
const const handleRedirect: () => voidhandleRedirect = () => {
const const url: URLurl = new var URL: new (url: string | URL, base?: string | URL) => URLThe URL interface represents an object providing static methods used for creating object URLs.
URL
class is a global reference for import URL from 'node:url'
https://nodejs.org/api/url.html#the-whatwg-url-api
URL(var window: Window & typeof globalThiswindow.location: Locationlocation.Location.href: stringhref);
const const bundle: string | nullbundle = const url: URLurl.URL.searchParams: URLSearchParamssearchParams.URLSearchParams.get(name: string): string | nullReturns the first value associated to the given search parameter.
get("bundle");
if (const bundle: string | nullbundle) {
const authenticate: (variables: AuthParams, options?: MutateOptions<User, Error, AuthParams, unknown> | undefined) => voidauthenticate(
{
type: "email"type: "email",
bundle: stringbundle,
},
{
MutateOptions<User, Error, AuthParams, unknown>.onSuccess?: ((data: User, variables: AuthParams, context: unknown) => void) | undefinedonSuccess: () => {
// onSuccess only fires once the entire flow is done (email magic link + optional MFA).
// It still runs even if the final step completes in another tab/window.
},
MutateOptions<User, Error, AuthParams, unknown>.onError?: ((error: Error, variables: AuthParams, context: unknown) => void) | undefinedonError: (error: Errorerror) => {
// Handle error
},
},
);
}
};
const handleRedirect: () => voidhandleRedirect();
}, [const authenticate: UseMutateFunction<User, Error, AuthParams, unknown>authenticate]);
Step 3: Track Authentication Status
Use the useSignerStatus
hook to determine if the user is authenticated:
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 } from "@account-kit/react";
// Inside your component
const { const isConnected: booleanisConnected } = function useSignerStatus(override?: AlchemyAccountContextProps): UseSignerStatusResultHook to get the signer status, optionally using an override configuration, useful if you’re building your own login.
useSignerStatus();
// You can use isConnected to conditionally render UI
Next Steps
Add Authenticator App (TOTP) Verification (Optional)
Consider enabling Multi-Factor Authentication to require users to enter a 6-digit TOTP code from their authenticator app after clicking the magic link. This extra layer of security protects user accounts if their email is compromised.