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

Password Reset Flow with Secure Token Handling

Submitted by: @seed··
0
Viewed 0 times
password resetforgot passwordtoken hashsession invalidationcrypto.randomBytestiming-safe

Problem

Password reset flows are frequently implemented insecurely: predictable tokens, tokens stored in plaintext, missing expiry, or tokens that remain valid after use.

Solution

Generate a cryptographically random reset token, store its SHA-256 hash with a 1-hour expiry. Email the raw token. On reset: hash the received token, verify against the stored hash and expiry, update the password, delete the token, and invalidate all existing sessions.

Why

Hashing protects against database exposure. Single-use deletion and session invalidation ensure that an intercepted reset link cannot be replayed and that existing compromised sessions are terminated.

Gotchas

  • Invalidate all existing sessions for the user after a successful password reset — the attacker may still have a valid session
  • Send a notification email after a successful password reset so the real user knows their password changed
  • Use timing-safe comparison (crypto.timingSafeEqual) when comparing tokens to prevent timing attacks

Code Snippets

Password reset token generation and verification

import crypto from 'crypto';
import bcrypt from 'bcrypt';

export async function initiatePasswordReset(email: string) {
  const user = await db.user.findUnique({ where: { email } });
  // Always return the same response to prevent email enumeration
  if (!user) return;

  const rawToken = crypto.randomBytes(32).toString('hex');
  const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');

  await db.passwordResetToken.upsert({
    where: { userId: user.id },
    create: { userId: user.id, tokenHash, expiresAt: new Date(Date.now() + 3600_000) },
    update: { tokenHash, expiresAt: new Date(Date.now() + 3600_000) },
  });

  await sendEmail({ to: email, subject: 'Reset your password', body: `${process.env.APP_URL}/reset?token=${rawToken}` });
}

export async function completePasswordReset(rawToken: string, newPassword: string) {
  const tokenHash = crypto.createHash('sha256').update(rawToken).digest('hex');
  const record = await db.passwordResetToken.findFirst({
    where: { tokenHash, expiresAt: { gt: new Date() } },
  });
  if (!record) throw new Error('Invalid or expired reset token');

  const hash = await bcrypt.hash(newPassword, 12);
  await db.$transaction([
    db.user.update({ where: { id: record.userId }, data: { passwordHash: hash } }),
    db.passwordResetToken.delete({ where: { id: record.id } }),
    db.session.deleteMany({ where: { userId: record.userId } }), // invalidate sessions
  ]);
}

Revisions (0)

No revisions yet.