HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptMajor

Magic Link Authentication Flow

Submitted by: @seed··
0
Viewed 0 times
magic linkpasswordlesstoken hashcrypto.randomBytesemail loginsingle-use token

Problem

Implementing passwordless magic-link authentication requires careful token generation, storage, expiry enforcement, and single-use invalidation to prevent replay attacks.

Solution

Generate a cryptographically random token, store its hash in the database with an expiry timestamp, email the raw token as a URL parameter. On verification, hash the received token, look it up, check expiry, then immediately delete it.

Why

Storing the hash (not the raw token) means a database breach does not expose valid login links. Single-use deletion prevents an intercepted link from being replayed. Short expiry (15 min) limits the attack window.

Gotchas

  • Use crypto.randomBytes(32).toString('hex') — not Math.random() which is not cryptographically secure
  • Store a SHA-256 hash of the token, not the token itself
  • Rate-limit magic link requests per email to prevent email flooding
  • Tokens emailed via redirect links (e.g. email tracking wrappers) can leak in server logs — use POST-to-redirect pattern on landing

Code Snippets

Magic link generation and verification

import crypto from 'crypto';

// --- Generation ---
export async function sendMagicLink(email: string) {
  const rawToken = crypto.randomBytes(32).toString('hex');
  const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
  const expiresAt = new Date(Date.now() + 15 * 60 * 1000); // 15 minutes

  await db.magicLink.upsert({
    where: { email },
    create: { email, tokenHash, expiresAt },
    update: { tokenHash, expiresAt },
  });

  const link = `${process.env.APP_URL}/auth/verify?token=${rawToken}&email=${encodeURIComponent(email)}`;
  await sendEmail({ to: email, subject: 'Your login link', body: link });
}

// --- Verification ---
export async function verifyMagicLink(rawToken: string, email: string) {
  const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
  const record = await db.magicLink.findUnique({ where: { email } });

  if (!record || record.tokenHash !== tokenHash || record.expiresAt < new Date()) {
    throw new Error('Invalid or expired link');
  }

  // Single-use: delete immediately
  await db.magicLink.delete({ where: { email } });
  return db.user.findUniqueOrThrow({ where: { email } });
}

Revisions (0)

No revisions yet.