惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

F
Fortinet All Blogs
S
Secure Thoughts
月光博客
月光博客
美团技术团队
雷峰网
雷峰网
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
N
News and Events Feed by Topic
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Forbes - Security
Forbes - Security
W
WeLiveSecurity
P
Proofpoint News Feed
阮一峰的网络日志
阮一峰的网络日志
爱范儿
爱范儿
G
GRAHAM CLULEY
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
AI
AI
Last Week in AI
Last Week in AI
Google Online Security Blog
Google Online Security Blog
Schneier on Security
Schneier on Security
云风的 BLOG
云风的 BLOG
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Recent Announcements
Recent Announcements
Webroot Blog
Webroot Blog
T
Tor Project blog
Cisco Talos Blog
Cisco Talos Blog
N
News and Events Feed by Topic
罗磊的独立博客
The Register - Security
The Register - Security
Blog — PlanetScale
Blog — PlanetScale
T
Threat Research - Cisco Blogs
博客园 - 【当耐特】
Apple Machine Learning Research
Apple Machine Learning Research
人人都是产品经理
人人都是产品经理
T
The Exploit Database - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com
B
Blog
腾讯CDC
Microsoft Azure Blog
Microsoft Azure Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Engineering at Meta
Engineering at Meta
Latest news
Latest news
IT之家
IT之家
D
DataBreaches.Net
博客园 - 司徒正美
N
Netflix TechBlog - Medium
V
V2EX
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知

Clerk Blog

Going to production with Clerk Deploy Clerk Init: The fastest way to start a new project Introducing Clerk CLI Middleware-based route protection bypass Postmortem: Clerk System Outage (March 10, 2026) Clerk for the AI era Add API Key support to your SaaS in minutes Postmortem: Clerk System Outage (February 19, 2026) Using Clerk in a React Native app Postmortem: DNS Provider Outage (February 10, 2026) How do I implement passkeys in Next.js? Clerk ranked #4 fastest-growing software vendor on Ramp’s December 2025 list How do I handle JWT verification in Next.js? Committing to Agent Identity: Clerk raises $50m Series C from Menlo and Anthropic’s Anthology Fund What is the best way to handle authentication in Next.js App Router? Postmortem: Database Incident (September 14–18, 2025) How do I add authentication to a Next.js app? Introducing Free Trials in Clerk Billing Postmortem: August 28, 2025 - elevated API latency and errors Introducing Mosaic: Bring Your Brand to Every Authentication Flow Multi-tenant authentication: What you need to know (and how Clerk helps) What are the risks and challenges of multi-tenancy? Resilience in Practice: Regional Failover at Clerk Build a Cross-Platform B2B App with Clerk, Expo, and Supabase Highlights from the MiduDev/Clerk Hackathon Add multi-tenancy to an app built with Clerk, Lovable, and Supabase How to build an AI coding rules app with Clerk, Lovable, and Supabase How to Build Multi-Tenant Authentication with Clerk Choosing the right SaaS architecture: Multi-Tenant vs. Single-Tenant Postmortem: June 26, 2025 service outage How to Design a Multi-Tenant SaaS Architecture What is multi-tenancy and why it matters for B2B SaaS How OAuth Works Synchronize user data from Clerk to Supabase Add subscriptions to your SaaS with Clerk Billing Getting started with Clerk Billing Multi-tenant analytics with Tinybird and Clerk How Huntr Migrated 250K Users to Clerk: A Scalable Auth Solution for Startups How to take Clerk to Production How to take your Clerk application to production A practical guide to testing Clerk Next.js applications Implementing multi-tenancy into a Supabase app with Clerk How Clerk integrates with a Next.js application using Supabase How Clerk integrates with Supabase Build a blog with tRPC, Prisma, Next.js and Clerk How to enrich PostHog events with Clerk user data How to build a secure project management platform with Next.js, Clerk, and Neon Validate your SaaS idea while building an audience Postmortem: February 6, 2025 service outage Implement Role-Based Access Control in Next.js 15 Build a Next.js sign-up form with React Hook Form Build a Next.js login page template How to implement Google authentication in Next.js 15 What is middleware in Next.js? How to customize Next.js metadata How to set environment variables in Node.js Building a React Login Page Template How to implement per-user OAuth scopes with Clerk Using Clerk SSO to access Google Calendar and other service data Streamline enterprise customer onboarding with SAML and Clerk Clerk launches EASIE SSO and eliminates SSO fees How to secure Liveblocks Rooms with Clerk in Next.js Securing Node.js Express APIs with Clerk and React Combining the benefits of session tokens and JWTs Build a task manager with Next.js, Supabase, and Clerk Comparing Clerk Webhooks vs Backend API Automate Neon schema changes with Drizzle and GitHub Actions A guide to reading authenticated user data from Clerk Role based access control with Clerk Organizations Mitigating OAuth’s recently discovered Open Response Type vulnerability Per-user B2B monetization with Stripe and Clerk Organizations Build a team-based task manager with Next.js, Neon, and Clerk Building a Hybrid Sign-Up/Subscribe Form with Stripe Elements Welcoming Colin from Zod as our inaugural Open Source Fellow Build a modern authenticated chat application with Next.js, Ably, and Clerk Build a waitlist with Clerk user metadata How to use Clerk with PostHog Identify in Next.js How to secure API Gateway using JWT and Lambda Authorizers with Clerk What are passkeys and how do they work? Comparing Authentication in React.js vs. Next.js How to Add an Onboarding Flow for your Application with Clerk Create Your Own Custom User Menu with Radix - Part 2 Introducing Webhook Workflows with Inngest & Svix Clerk raises $30M Series B from CRV and Stripe Clerk in 2023: A Year in Review Build a Movie Emoji Quiz App with Remix, Fauna, and Clerk Create Your Own Custom User Menu with Radix Introducing has(), protect(), and <Protect> Updated Pricing: 10,000 MAUs Free, and a new “Pro Plan” Next.js Authentication with Clerk: Streamlined SSR Handling Clerk Webhooks: Data Sync with Convex Exploring Clerk Metadata with Stripe Webhooks The Ultimate Guide to Next.js Authentication Empower Your Support Team With User Impersonation Clerk Webhooks: Getting Started A Complete Guide to Session Management in Next.js The Advanced Guide to Passwordless Authentication in Next.js How We Roll – Chapter 10: Roundup How We Roll – Chapter 9: Infrastructure Password-Based Authentication in Next.js
Ultimate Guide to Magic Link Authentication
Nick Parsons · 2023-12-21 · via Clerk Blog

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, highlighting the critical requirements for improved security measures.

Luckily, email 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 authentication with 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. Authentication with 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:

https://example.com/authenticate?token=a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

In this example, the 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:

Example Magic Link Authentication Process

Token Validity

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.

Token Generation

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.

User Interaction and Verification

A user typically 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.

Session Initialization

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.

Let’s create our own email with magic link authentication. In this example, we’ll use Next.js 13 with the App Router. We’ll also use Supabase for our backend database to store users and tokens.

First, let’s create a new Next.js project:

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 localhost:3000.

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?

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.js will be our homepage. It will have a simple email field, and will call our requestMagicLink API reroute.
  • requestMagicLink.js will 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.js will 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.js will be a simple mock “protected page” (that would require the user to be logged in to view it).
  • lib/database.js will be a number of database helper functions to save, load, and delete Supabase data.
  • lib/supabaseClient.js will set up our Supabase client.

page.js

Let’s start with what the user will see, page.js:

In the code example above, we are requesting 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:

  • email: Stores the user's email address. It's updated every time the user types into the email input field.
  • 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 isLoading to true to indicate the start of an asynchronous operation and clears any previous messages stored in message.

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.ok is 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 isLoading to false.

The actual form presented to the user is basic, with just an email input field that updates the email state variable on change and a submit button that is disabled and changes its text based on 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 message state.

This page.js file calls requestMagicLink.js; let’s dig into that, next.

This example 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 email from the request's JSON body. We then use getUserByEmail from 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 saveToken.

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 authentication.

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.

database.js

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:

saveToken

  • Purpose: To save a newly generated token in the database, fulfilling the requirements for secure token management.
  • Parameters: Accepts userId (the user's ID) and token (the magic link token).
  • Functionality:
    • Inserts a new record into the magic_tokens table 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.

getUserByEmail

  • Purpose: To fetch a user's data from the database using their email address.
  • Parameters: Accepts email, which is the email address of the user.
  • Functionality:
    • Queries the users table 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.

getTokenData

  • Purpose: To retrieve token data from the database.
  • Parameters: Accepts token, the magic link token.
  • Functionality:
    • Queries the magic_tokens table for a record with a token matching the provided one.
    • Checks if the token has expired by comparing the expires_at field 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.

deleteUserToken

  • Purpose: To delete a token from the database, complying with the requirements for token lifecycle management.
  • Parameters: Accepts token, the magic link token to be deleted.
  • Functionality:
    • Deletes the record from the magic_tokens table 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.

This file, in turn, is calling on the other helper library supabaseClient.js.

supabaseClient.js

This is a straightforward setup for initializing a client instance of Supabase in a Javascript application.

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_URL and SUPABASE_ANON_KEY (along with JWT_SECRET) are pulled from our environment variables file, .env.local.

.env.local

You get SUPABASE_URL and 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 email field, and a 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.

verify.js

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 (req.url.split("=")[1]).

The 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 (email) from the token.

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.

dashboard/page.js

There isn’t much to dashboard/page.js:

In a production application, this is the page you would build out in your application.

Final Thoughts

There is a lot to think about with email 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 documentation or this article on implementing magic links with Next.js and Clerk.