SkillByAIOpen interactive version →

Lesson 6 / 25

Password Reset and Email Verification

Single-use, expiring, unguessable tokens.

Building a safe reset flow

Generate a cryptographically random token (at least 128 bits), store only its hash with the user ID and a short expiry, and email a link containing the raw token. When the link is used: look up by hash, check expiry, let the user set a new password, then delete the token (single use) and invalidate existing sessions. Always respond "If an account exists, we sent an email" to avoid enumeration. Build the link from a configured base URL, not the request Host header, which attackers can poison. Email verification works the same way: a random, expiring, single-use token proving control of the address. Never auto-login a user from a reset link without care, and never send passwords by email.

Issuing and redeeming a reset token

Node.js crypto; only the SHA-256 of the token is stored.

import crypto from 'node:crypto';

const sha256 = (s: string) => crypto.createHash('sha256').update(s).digest('hex');

export async function requestReset(email: string) {
  const user = await db.users.findByEmail(email);
  if (user) {
    const token = crypto.randomBytes(32).toString('base64url'); // 256 bits
    await db.resetTokens.insert({
      userId: user.id,
      tokenHash: sha256(token),
      expiresAt: new Date(Date.now() + 30 * 60 * 1000), // 30 minutes
    });
    await mailer.send(user.email, `${APP_BASE_URL}/reset?token=${token}`);
  }
  // same response whether or not the account exists
  return { message: 'If an account exists for that email, we sent a reset link.' };
}

export async function redeemReset(token: string, newPassword: string) {
  const row = await db.resetTokens.findByHash(sha256(token));
  if (!row || row.expiresAt < new Date()) throw new Error('invalid or expired link');
  await db.users.setPasswordHash(row.userId, await hashPassword(newPassword));
  await db.resetTokens.deleteAllForUser(row.userId); // single use
  await sessions.revokeAllForUser(row.userId);       // log out other devices
}

Why store a hash of the token?

Reset tokens are password-equivalent until they expire. Hashing them means a database leak or a read-only SQL injection does not hand attackers working reset links. A fast hash is fine here because the token is long and random.

Quick check: Which property should a password reset token NOT have?

  • Only its hash is stored server-side
  • It is generated with a cryptographically secure random source
  • It expires after a short time
  • It stays valid after it has been used once
Answer

It stays valid after it has been used once — Reset tokens must be single use and short-lived.