Node.js › Authentication & Authorization

How do you implement email verification and password-reset flows securely in a Node.js application?

Intermediate

Answer

Both flows rely on generating a cryptographically random, single-use token, storing only its hash in the database alongside an expiration timestamp, and emailing the user a link containing the raw token -- when the link is clicked, the submitted token is hashed and compared against the stored hash, and the action (verifying the email, or allowing a password reset) is only permitted if the hash matches and the token hasn't expired or already been used, after which the token is immediately invalidated to prevent reuse.
const crypto = require('node:crypto');

async function generateResetToken(userId) {
  const rawToken = crypto.randomBytes(32).toString('hex');
  const hashedToken = crypto.createHash('sha256').update(rawToken).digest('hex');
  await db.passwordResets.create({ userId, hashedToken, expiresAt: Date.now() + 3600000 });
  return rawToken; // only the raw token is emailed, never stored
}
Real-world example A SaaS product's password-reset emails contain a link with a raw random token that expires after one hour and can only be used once; even if the reset-tokens database table were compromised, an attacker would only see hashed tokens that can't be reversed back into a working reset link.

Common follow-ups: Why should the raw token never be stored in the database, mirroring the reasoning behind password hashing?;How do you prevent an attacker from enumerating valid email addresses through the password-reset request endpoint's response differences?