patterntypescriptMajor
Password Reset Flow with Secure Token Handling
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.