Passwordless Magic Link Auth
Email magic link flow with Resend and custom token verification
What You’ll Build
After following this guide, you will have a working implementation of passwordless magic link auth in your project. Build passwordless authentication where users click a link in their email to log in — no passwords needed. Uses Resend to send the email, a signed JWT as the magic token, and an Express endpoint to verify and create a session.
Use Cases & Problems Solved
- Protect routes so only authenticated users can access sensitive pages
- Allow users to sign up, log in, and manage their accounts securely
- Avoid storing raw passwords or building session management from scratch
Prerequisites
- Resend account with verified domain
- Node.js 18+
- Express.js project
Step-by-Step Implementation
Install dependencies
The following snippet shows how to install dependencies. Copy this into your project and adjust the values for your environment.
npm install resend jsonwebtoken cookie-parser
Send the magic link email
The following snippet shows how to send the magic link email. Copy this into your project and adjust the values for your environment.
const jwt = require('jsonwebtoken');
const { Resend } = require('resend');
const resend = new Resend(process.env.RESEND_API_KEY);
app.post('/auth/magic-link', async (req, res) => {
const { email } = req.body;
const token = jwt.sign({ email }, process.env.MAGIC_SECRET, { expiresIn: '15m' });
const link = `${process.env.APP_URL}/auth/verify?token=${token}`;
await resend.emails.send({
from: 'auth@yourdomain.com',
to: email,
subject: 'Your sign-in link',
html: `<a href="${link}">Click here to sign in</a><p>Expires in 15 minutes.</p>`,
});
res.json({ message: 'Check your email for the sign-in link' });
});
Verify the token and create session
The following snippet shows how to verify the token and create session. Copy this into your project and adjust the values for your environment.
app.get('/auth/verify', async (req, res) => {
try {
const { email } = jwt.verify(req.query.token, process.env.MAGIC_SECRET);
let user = await User.findByEmail(email);
if (!user) user = await User.create({ email });
const sessionToken = jwt.sign({ id: user.id }, process.env.SESSION_SECRET, { expiresIn: '7d' });
res.cookie('session', sessionToken, { httpOnly: true, secure: true, sameSite: 'lax' });
res.redirect('/dashboard');
} catch (err) {
res.status(400).send('Invalid or expired link. Please request a new one.');
}
});
⚠️ Don’t Do This
❌ Making magic links that never expire
// Never expires — if email is intercepted, attacker has permanent access
const token = jwt.sign({ email }, secret); // No expiresIn!
✅ Set a short expiration (10-15 minutes) and single-use tokens
const token = jwt.sign({ email }, secret, { expiresIn: '15m' });
// Also: mark token as used in DB after verification to prevent replay
Testing
Add these tests to verify your passwordless magic link auth implementation works correctly:
// __tests__/passwordless.test.ts
import { describe, it, expect } from 'vitest';
describe('Passwordless Magic Link Auth', () => {
it('should handle successful authentication', async () => {
// Test the happy path
const result = await authenticate({ email: 'test@example.com', password: 'valid' });
expect(result.user).toBeDefined();
expect(result.error).toBeNull();
});
it('should reject invalid credentials', async () => {
const result = await authenticate({ email: 'test@example.com', password: 'wrong' });
expect(result.user).toBeNull();
expect(result.error).toBeDefined();
});
it('should handle missing fields', async () => {
const result = await authenticate({ email: '', password: '' });
expect(result.error).toBeDefined();
});
});
Verification
npm start
# POST to /auth/magic-link with your email
curl -X POST http://localhost:3000/auth/magic-link \
-H 'Content-Type: application/json' -d '{"email":"you@example.com"}'
# Check your inbox and click the link Related Specs
Supabase Email/Password Auth
Sign up, sign in, password reset, and protected routes with Supabase Auth
Clerk Auth in Next.js App Router
Drop-in auth with Clerk, middleware protection, and user metadata
Next.js Google OAuth Login
Complete Google OAuth flow with NextAuth.js, token handling, session management