Script Valley
Authentication From Scratch
Password Reset and MFALesson 6.1

How to build a secure password reset flow

password reset token generation, token storage and expiry, reset email, constant-time comparison, token invalidation after use, account enumeration prevention

The Reset Flow

A password reset flow is a temporary, out-of-band authentication mechanism. The email inbox acts as proof of identity. A compromised email means a compromised account — that is the security boundary.

// POST /auth/forgot
app.post('/auth/forgot', async (req, res) => {
  const { email } = req.body;
  // Always return 200 regardless of whether email exists
  res.json({ message: 'If that email exists, a reset link was sent.' });

  // Do the real work asynchronously after responding
  const user = await db.findByEmail(email);
  if (!user) return; // silently do nothing

  const token = crypto.randomBytes(32).toString('hex');
  const expiry = new Date(Date.now() + 60 * 60 * 1000); // 1 hour

  await db.storeResetToken(user.id, token, expiry);
  await sendResetEmail(user.email, token);
});

Validating the Reset

app.post('/auth/reset', async (req, res) => {
  const { token, newPassword } = req.body;
  const user = await db.findByResetToken(token);

  if (!user || user.resetExpiry < new Date()) {
    return res.status(400).json({ error: 'Invalid or expired token' });
  }

  const hash = await bcrypt.hash(newPassword, 12);
  await db.updatePassword(user.id, hash);
  await db.clearResetToken(user.id);
  res.json({ message: 'Password updated' });
});

Respond with 200 before checking if the email exists. This prevents account enumeration via timing or response differences.

Up next

What is TOTP and how two-factor authentication works

Sign in to track progress