Data breaches and password overload have made companies and their users wary of using a traditional username/password authentication system. Companies know that handling user passwords is both technically challenging and costly, as it requires stringent security measures, robust infrastructure for storage, and continuous monitoring to prevent unauthorized access. Users find it cumbersome to manage multiple complex passwords for various accounts, especially with the prevalence of methods like SSO, and often end up reusing passwords across different platforms, as a matter of convenience; this not only leaves the individual vulnerable, but also makes companies jobs of securing data that much harder.
Luckily, magic links have emerged as an elegant solution to secure user sessions in a passwordless context. Their rising popularity is underpinned by their dual advantages:
- An augmented security posture through the minimization of phishing opportunities and password theft.
- An optimized user experience devoid of the need to memorize and manage many login credentials.
Magic links are a token-based authentication (TBA) strategy that uses a unique, time-sensitive URL, which leverages a securely-generated token to serve as a credential for user authentication.
The links are sent directly to the user's registered email or phone number, providing a straightforward, secure authentication method. When a user clicks on a magic link, the embedded token is validated against the server to authenticate the user's identity. This process, by design, eliminates the traditional risks associated with password-based systems and simplifies the login experience for the user.
In this guide, we will show you why you should consider magic links and how they work at a high level, before going through a Next.js App Router implementation to show how you can add magic links to your application.
Magic links bolster the security architecture of authentication systems by adopting a token-based, stateless interaction model. Each link is cryptographically unique and typically accompanied by an expiration timestamp, making it resilient against replay attacks. Given their ephemeral nature, even if a magic link were to be intercepted or exposed, its short-lived validity constrains the window of opportunity for malicious exploitation.
But there are a few other benefits to magic links for companies and users:
- Streamlined user experience. Magic links eliminate the cognitive burden of remembering and managing many complex passwords, by providing a single-click authentication pathway. This frictionless access modality can enhance user engagement and satisfaction, reducing abandonment rates during the sign-in or sign-up processes.
- Compliance and data protection. By using magic links to minimize passwords, organizations can reduce the risk of breaches involving personal data and avoid the consequences of compromised credentials, which can include heavy fines and reputational damage.
- Reduction in credential exposure. With magic links, the threat of credential stuffing attacks—where compromised credentials are used to gain unauthorized access to multiple user accounts—is mitigated. Since magic links do not rely on reusable passwords, the standard vector of credential exposure is eliminated, enhancing overall system security.
- Improved accessibility. For users with disabilities or those less familiar with technology, magic links offer a more accessible authentication mode. The simplification of the login process—replacing typing and memory demands with a single action—can be particularly advantageous for individuals facing physical or cognitive challenges.
A magic link is structurally composed of two critical elements: the URL, which provides the link for the user's web interaction, and the embedded token, a cryptographically-generated string serving as the temporary credential.
In essence, a typical magic link may resemble the following structure:
token query parameter carries the weight of authentication, substantiating the user's claim without revealing identity until verified by the server.
Here’s an example of how the process works:
Magic links are designed to become invalid under certain conditions for security purposes:
- Expiration: The token embedded in the link has a short lifespan and is often configurable based on application security requirements.
- Usage Limitation: Once a magic link is used, it becomes invalid, preventing multiple uses, which could lead to unauthorized access.
- Revocation: The system can programmatically revoke a token, thus invalidating the magic link in response to specific triggers or anomalies.
The lifecycle of a magic link commences with the generation of a unique token. This process employs cryptographic algorithms to ensure each token is a random, high-entropy string, making it virtually impossible to predict or reproduce through brute force or other cryptographic attacks. Typically, the token generation utilizes HMAC or AES combined with a CSPRNG to guarantee the robustness of the token against collision and preimage attacks.
Once generated, the token is stored on the server along with metadata that includes the user's identifier, the token's expiration time, and any other relevant session data. The magic link is then composed by appending the token to a predetermined URL structure, forming a complete, ready-to-use hyperlink.
This link is dispatched to the user's email address or phone number via SMTP or SMS protocols. The communication channel must be secure, leveraging TLS for email and similarly secure protocols for SMS to safeguard the link during transit.
pically interacts with the magic link by clicking on it, which initiates a secure request to the service's endpoint. The service extracts the token from the URL and verifies it against the stored data. This verification process involves several checks:
- Authenticity: Validates that the token matches a recently generated and stored token.
- Integrity: Ensures the token has not been tampered with during transit.
- Timeliness: Confirms the token has not expired based on the predefined validity period.
- Non-reuse: Ensures the token has not been used before, adhering to the principle of one-time use.
The server considers the authentication request legitimate if the token passes these checks.
Post-verification, the server establishes a session for the user. This session is typically stateless, with a new session token or cookie generated to maintain the user's authenticated state in the application. This session token is separate from the magic link token. It has its own security considerations, such as being HttpOnly and Secure, to prevent access via client-side scripts and ensure transmission over HTTPS only.
First, let’s create a new Next.js project:
Follow the prompts to select how you want to configure your app, but be sure to select “Yes” for “Would you like to use App Router? (recommended)”.
Once you have created and configured your project,
cd into the created directory and run
npm run dev to start it. You’ll see just the default Next.js homepage when you load
Before we start building out our project, we need to install a few dependencies that we’ll use. Install them using:
What do these do?
- nodemailer: A library that allows for easy email sending.
- jsonwebtoken: A library to implement JSON Web Tokens for secure data transmission.
With those installed, let’s open up the project in an IDE. In total, we’re going to have two pages, two API routes, and two helper libraries:
page.jswill be our homepage. It will have a simple email field, and will call our
requestMagicLink.jswill be the API route that will create our magic link, send it to the email address passed from
page.js, and save the token on our Supabase database.
verify.jswill be the API route called when the user clicks on the magic link in their email. It will verify the token and redirect the user to the protected dashboard page.
dashboard/page.jswill be a simple mock “protected page” (that would require the user to be logged in to view it).
lib/database.jswill be a number of database helper functions to save, load, and delete Supabase data.
lib/supabaseClient.jswill set up our Supabase client.
Let’s start with what the user will see, page.js:
The main part of this code is the
RequestMagicLink component, which is responsible for handling the functionality of requesting a magic link via an email address. This component provides a UI for users to request a magic link. Users enter their email and submit the form, triggering a request to the server. Feedback is given to the user through messages and button state changes during the process.
First, we set up the state variables for the page. In the Next.js App Router, you can only use
useState in client-side components. As all components default to server-side, we have to use the
“use client” directive at the top of the file to show this page has to be rendered on the client. We have three state variables:
message: Used to display messages to the user, like confirmation or error messages.
isLoading: Indicates whether the request is being processed. It's used to disable the submit button and change the button text while the request is in progress.
After that, we have the
handleSubmit function. This is triggered when the form is submitted, and initially prevents the default form submission action with
event.preventDefault(). It then sets
true to indicate the start of an asynchronous operation and clears any previous messages stored in
Then, comes the core part of the component. We make an async
POST request to the
/api/auth/requestMagicLink endpoint with the user's email in the request body. We then update the
message state based on the success or failure of the request:
- If the request is successful (
response.okis true), it sets a success message.
- If the request fails, it displays an error message, either from the response or a generic error message.
We have some basic error catching, and then set
The actual form presented to the user is basic, with just an email input field that updates the
isLoading state. We have an
onSubmit event handler linked to
handleSubmit to send the form details and a paragraph that displays any messages stored in the
page.js file calls
requestMagicLink.js; let’s dig into that, next.
This code handles the API requests related to generating and sending the magic link for user authentication. Let's go through the major components and functions of the code.
The function takes
req (request) and
res (response) objects as parameters, and then extracts the
lib/database.js to search for a user in the database using the provided email. If the user doesn’t exist we send a JSON response indicating the user was not found.
The function then creates the token using
jsonwebtoken to create a JWT with the user's email, signing it with a secret from environment variables and setting an expiration of 1 hour. With that token we can create our magic link. We retrieve the host from the request headers and construct a URL with the generated token as a query parameter.
The generated token is then saved in the database with an associated user ID using
After that, we configure a
nodemailer transporter with SMTP settings (host, port, security, and authentication credentials) and send an email to the user with the magic link.
Finally, we send back a JSON response indicating that the magic link was sent.
Here, we using one of our helper libraries,
database.js. Let’s go through that next.
This is a collection of utility functions designed to interact with Supabase. Each function is designed to interact with specific tables in a Supabase database, which each handle different aspects like token generation, user lookup, token validation, and cleanup. Let's break down each function:
- Purpose: To save a newly generated token in the database.
- Parameters: Accepts
userId(the user's ID) and
token(the magic link token).
- Inserts a new record into the
magic_tokenstable in Supabase with the user's ID, the token, and an expiration time (set to 1 hour ahead of the current time).
- If an error occurs during the database operation, it throws an error with the message received from Supabase.
- Returns the data received from Supabase if the operation is successful.
- Inserts a new record into the
- Purpose: To fetch a user's data from the database using their email address.
- Parameters: Accepts
- Queries the
userstable in Supabase for a record matching the provided email address.
- Logs the email and the data received for debugging purposes.
- If an error occurs, it throws an error with the message from Supabase.
- Returns the user data if a matching record is found.
- Queries the
- Purpose: To retrieve token data from the database.
- Parameters: Accepts
token, the magic link token.
- Queries the
magic_tokenstable for a record with a token matching the provided one.
- Checks if the token has expired by comparing the
expires_atfield with the current time. If the token is expired, it throws an error.
- If an error occurs during the query, it throws an error with the message from Supabase.
- Returns the token data if a matching and non-expired token is found.
- Queries the
- Purpose: To delete a token from the database.
- Parameters: Accepts
token, the magic link token to be deleted.
- Deletes the record from the
magic_tokenstable that matches the provided token.
- If an error occurs during this operation, it throws an error with the message from Supabase.
- Returns the data received from Supabase upon successful deletion.
- Deletes the record from the
This file, in turn, is calling on the other helper library
export const supabase = createClient(supabaseUrl, supabaseAnonKey);creates and exports an instance of the Supabase client, which is used to interact with your Supabase project. The
createClient function takes the Supabase URL and the anonymous key as arguments and returns the initialized client.
SUPABASE_ANON_KEY (along with
JWT_SECRET) are pulled from our environment variables file,
SUPABASE_ANON_KEY from your Supabase dashboard.
You’ll see from above, we also need to set up two tables in Supabase. We need a
users table with an
magic_tokens table with these fields:
user_id, which comes from the users table.
token, which is the generated token.
expires_at, to add an expiry time to the token.
Let’s get back to the main code. If we fill out the email field on the homepage and click “Send Magic Link,”
requestMagicLink will be called and an email sent to the entered email address. Clicking on that link calls the verify.js endpoint.
This defines the API route for verifying the magic link token as part of an authentication process.
The code is designed to:
- Extract a token from the request URL.
- Verify the token's validity.
- Perform actions based on the token's validity (such as user redirection or error response).
The code retrieves the token from the URL query string by splitting the URL at the
= character and taking the second part (
getTokenData function is called to fetch the token data from the database. If no data is found (implying the token is invalid or expired), it returns a JSON response with an error message. Using
jwt.verify, we validate the token against the secret key stored in
process.env.JWT_SECRET. This also extracts the payload (
We then call
deleteUserToken to remove the token from the database, ensuring it cannot be reused. Finally, it redirects the user to the
/dashboard route upon successful token verification.
There isn’t much to
In a production application, this is the page you would build out in your application.
There is a lot to think about with magic links. The above example doesn’t go into robust authentication checking with the user, nor does it add rate-limiting or have significant error handling. An unfortunate truth of authentication is that there is no silver bullet. While magic links take away the headache of managing user credentials, you still have to manage token generation, storage and expiry. Plus, you are still managing and storing user data.
Any good developer is going to take the time to understand, at least, the basic strategies leveraged by the tools they use to speed up their processes (good work understanding magic links!). With that being said, a simpler and more secure alternative to all the code above is to use Clerk. We built Clerk to make it quick and easy to add advanced authentication techniques into your application. To learn more about magic links, visit our magic link documentation or this magic link implementation article on implementing magic links with Next.js and Clerk.