Prompt

How to implement passwordless auth emails

Technology / Api platforms1 observationLast seen Aug 13, 2026

Latest observation

Aug 13, 2026 · OpenAI APIWeb search: off

Implementing passwordless auth emails usually means one of two patterns:

  1. Magic link: user enters email, you send a link with a one-time token.
  2. Email code / OTP: user enters email, you send a short code they type back in.

For most apps, magic links are the easiest and best UX. Here’s a practical implementation approach.


1) Flow overview

Magic link flow

  1. User enters email on a login page.
  2. Your backend generates a random one-time token.
  3. Store a hashed version of that token in your DB with:
    • email
    • expiration time
    • used/revoked flag
  4. Email the user a link like: https://yourapp.com/auth/verify?token=...
  5. User clicks link.
  6. Backend verifies token, checks expiry and unused status.
  7. If valid, mark token used and create a session / JWT.

2) Recommended security practices

  • Use cryptographically secure random tokens
    • e.g. 32+ bytes random
  • Store only a hash of the token
    • if DB leaks, raw tokens aren’t exposed
  • Expire tokens quickly
    • 10–15 minutes is common
  • Single-use tokens
    • mark as used immediately after login
  • Rate limit requests
    • prevent email abuse / enumeration
  • Avoid revealing whether an email exists
    • always respond with a generic message like “If that email exists, we sent a link”
  • Bind token to purpose
    • separate tokens for login vs password reset
  • Use HTTPS
  • Use session cookies securely
    • HttpOnly, Secure, SameSite

3) Database design

Example table:

CREATE TABLE auth_tokens (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  token_hash VARCHAR(255) NOT NULL,
  purpose VARCHAR(50) NOT NULL DEFAULT 'login',
  expires_at TIMESTAMP NOT NULL,
  used_at TIMESTAMP NULL,
  created_at TIMESTAMP NOT NULL DEFAULT NOW()
);

If you already have a users table, you can attach the email to an existing user or create one on first login.


4) Token generation

Example in Node.js:

import crypto from "crypto";

function generateToken() {
  return crypto.randomBytes(32).toString("hex");
}

Hashing the token before storing:

function hashToken(token) {
  return crypto.createHash("sha256").update(token).digest("hex");
}

5) Request login endpoint

POST /auth/magic-link

User submits email.

Pseudo-flow:

  • Validate email format
  • Create token
  • Store hash + expiry
  • Send email with link
  • Return generic success response

Example:

app.post("/auth/magic-link", async (req, res) => {
  const { email } = req.body;

  // Always respond generically
  const token = crypto.randomBytes(32).toString("hex");
  const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
  const expiresAt = new Date(Date.now() + 15 * 60 * 1000);

  await db.query(
    `INSERT INTO auth_tokens (email, token_hash, purpose, expires_at)
     VALUES ($1, $2, 'login', $3)`,
    [email.toLowerCase(), tokenHash, expiresAt]
  );

  const link = `https://yourapp.com/auth/verify?token=${token}&email=${encodeURIComponent(email)}`;

  await sendEmail(email, "Your login link", `Click here to sign in: ${link}`);

  return res.json({ message: "If that email exists, we sent a login link." });
});

6) Verify endpoint

GET /auth/verify?token=...&email=...

You can verify via token alone, but including email helps lookup and reduces ambiguity.

Pseudo-flow:

  • Hash token from query
  • Find matching unused token record
  • Check expiry
  • Mark used
  • Create session / JWT
  • Redirect user to app

Example:

app.get("/auth/verify", async (req, res) => {
  const { token, email } = req.query;

  if (!token || !email) {
    return res.status(400).send("Invalid link");
  }

  const tokenHash = crypto.createHash("sha256").update(token).digest("hex");

  const result = await db.query(
    `SELECT * FROM auth_tokens
     WHERE email = $1 AND token_hash = $2 AND purpose = 'login' AND used_at IS NULL
     LIMIT 1`,
    [String(email).toLowerCase(), tokenHash]
  );

  const row = result.rows[0];
  if (!row) return res.status(400).send("Invalid or expired link");

  if (new Date(row.expires_at) < new Date()) {
    return res.status(400).send("Invalid or expired link");
  }

  await db.query(
    `UPDATE auth_tokens SET used_at = NOW() WHERE id = $1`,
    [row.id]
  );

  // Create session cookie or JWT
  const sessionToken = createSessionForUser(email);

  res.cookie("session", sessionToken, {
    httpOnly: true,
    secure: true,
    sameSite: "lax"
  });

  return res.redirect("/app");
});

7) Email content

Keep it simple and clear:

Subject: Your sign-in link

Body:

  • Mention expiry time
  • Explain user should only use it if they requested it
  • Provide a fallback code or support contact if needed

Example:

Click the link below to sign in.
This link expires in 15 minutes and can only be used once.
If you didn’t request this, you can ignore this email.


8) Better UX option: magic link + short code fallback

Some users open mail in a different device or app where links are awkward. You can include:

  • a clickable magic link
  • a 6-digit code they can enter manually

This gives flexibility.


9) If you want a more secure setup

Instead of putting the raw token in the URL, you can:

  • send a link to a frontend page
  • that page calls backend with the token
  • backend verifies and sets session

This avoids accidentally leaking tokens through logs or referrers.

Also consider:

  • set Referrer-Policy: no-referrer
  • don’t log full query strings
  • use short-lived tokens

10) Common pitfalls

  • Email enumeration: avoid “user not found” messages
  • Token reuse: always mark used
  • Long expiry: keep it short
  • Not hashing tokens: risky if DB leaks
  • Using weak random values: avoid Math.random()
  • No rate limiting: can spam emails
  • Storing auth token in localStorage: prefer HttpOnly cookies

11) If you’re using an auth provider

If you don’t want to build this yourself, many providers support it:

  • Auth0
  • Clerk
  • Supabase Auth
  • Firebase Auth
  • Magic.link
  • Cognito

They handle:

  • token generation
  • email delivery
  • verification
  • sessions
  • abuse mitigation

12) Minimal implementation checklist

  • Login form collects email
  • Backend generates secure token
  • Store hashed token + expiry
  • Send email with verification link
  • Verify token on click
  • Mark token used
  • Create session cookie
  • Rate limit requests
  • Generic responses
  • Short expiration

If you want, I can also give you:

  • a Node/Express full example
  • a Next.js implementation
  • a Python/FastAPI example
  • or a Postgres schema + email template

Observation history

Unlock observation history

Create a free Obsurfable account to unlock historical responses, model comparisons, and deeper brand intelligence.

How did Obsurfable measure this prompt?

Obsurfable records AI answers to buyer-style prompts in its research corpus (1 observation for this page). Metrics are distributions over observations, not a single static ranking.

Which AI systems does Obsurfable collect answers from?

OpenAI, ChatGPT, Google, Gemini, Google AI Mode, Anthropic, Claude, Perplexity, Grok, DeepSeek, Mistral, Copilot, and Meta AI.