Passwordless Auth: Magic Links and OTP Without the Complexity
Passwords are the weakest link in auth. Users reuse them, forget them, and phishing steals them. Magic links and OTPs remove the password entirely.
Magic Link Flow
- User enters email
- Server generates a short-lived token, emails a link
- User clicks link, server validates token, creates session
- Token is single-use and expires in 15 minutes
Implementation
import { randomBytes } from 'crypto';
async function sendMagicLink(email: string) {
// Generate a secure random token
const token = randomBytes(32).toString('hex');
const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 min
// Store token hash (never store plaintext tokens)
const tokenHash = createHash('sha256').update(token).digest('hex');
await db.magicLinks.create({
data: { email, tokenHash, expiresAt, used: false },
});
// Send email with link
const link = `${process.env.APP_URL}/auth/verify?token=${token}`;
await resend.emails.send({
from: 'auth@yourapp.com',
to: email,
subject: 'Your login link',
html: `<a href='${link}'>Click to sign in</a> (expires in 15 minutes)`,
});
}
async function verifyMagicLink(token: string) {
const tokenHash = createHash('sha256').update(token).digest('hex');
const link = await db.magicLinks.findFirst({
where: { tokenHash, used: false, expiresAt: { gt: new Date() } },
});
if (!link) throw new Error('Invalid or expired link');
// Mark as used — prevents replay attacks
await db.magicLinks.update({
where: { id: link.id },
data: { used: true },
});
// Get or create user
const user = await db.users.upsert({
where: { email: link.email },
create: { email: link.email },
update: {},
});
return createSession(user);
}
OTP (One-Time Password)
function generateOTP(): string {
// 6-digit code, cryptographically random
return String(randomInt(100000, 999999));
}
async function sendOTP(email: string) {
const otp = generateOTP();
const expiresAt = new Date(Date.now() + 10 * 60 * 1000); // 10 min
// Store hashed OTP
await redis.setex(
`otp:${email}`,
600, // 10 minutes TTL
createHash('sha256').update(otp).digest('hex')
);
await sendSMS(email, `Your code: ${otp}`);
}
NextAuth Passwordless Setup
// NextAuth handles magic links natively
providers: [
EmailProvider({
server: process.env.EMAIL_SERVER,
from: process.env.EMAIL_FROM,
// Optional: custom token expiry
maxAge: 15 * 60, // 15 minutes
}),
],
Passwordless auth with NextAuth, magic links, and session management are pre-built in the AI SaaS Starter Kit.
Top comments (0)