How to Handle Password Resets in Node.js
Password reset is a common feature with security pitfalls. Here is how to do it right.
How to Handle Password Resets in Node.js
Password reset is a common feature with real security pitfalls. Here is how to do it right.
The Flow
- User clicks "Forgot password" and submits their email.
- Server generates a one-time reset token (random, unguessable).
- Server stores the hash of the token with an expiry (e.g., 15 min) on the user.
- Server emails the user a link containing the token (e.g., /reset-password?token=abc).
- User clicks the link, enters a new password.
- Server verifies the token (compare hash, check expiry), hashes the new password, saves, and invalidates the token.
Generate a Token
const crypto = require('crypto');
const token = crypto.randomBytes(32).toString('hex');
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const expiresAt = new Date(Date.now() + 15 * 60 * 1000);
Store the hash, not the raw token. If the database is leaked, attackers cannot use the tokens.
Store on User
user.resetTokenHash = tokenHash;
user.resetTokenExpiresAt = expiresAt;
await user.save();
Email the Link
await sendEmail({
to: user.email,
subject: 'Password reset',
body: `Click here to reset your password: https://app.com/reset-password?token=${token}`
});
The raw token goes in the URL. The hash is stored. An attacker who reads the database cannot use the hash.
Verify on Reset
router.post('/reset-password', asyncHandler(async (req, res) => {
const { token, newPassword } = req.body;
const tokenHash = crypto.createHash('sha256').update(token).digest('hex');
const user = await User.findOne({ resetTokenHash: tokenHash, resetTokenExpiresAt: { $gt: Date.now() } });
if (!user) return res.status(400).json({ error: 'Invalid or expired token' });
user.passwordHash = newPassword;
user.resetTokenHash = undefined;
user.resetTokenExpiresAt = undefined;
await user.save();
res.json({ message: 'Password reset' });
}));
The pre('save') hook hashes the new password. The token is invalidated by clearing the fields.
Security Considerations
- Token expiry: 15 min is good. Do not use 24 hours.
- One-time use: invalidate the token after a successful reset.
- Hash the stored token: never store raw tokens.
- Rate limit the request endpoint: prevent email flooding.
- Do not reveal if the email exists: return the same response whether the email is registered or not, to prevent enumeration.
- Notify the user after reset: send an email saying the password was changed.
- Invalidate sessions: after a reset, invalidate all existing JWTs (e.g., bump a tokenVersion field).
Common Mistakes
- Storing raw tokens instead of hashes.
- Long expiry (24 hours).
- Reusing tokens (no invalidation after reset).
- Revealing whether the email is registered.
- Not rate limiting the request endpoint.
The Takeaway
Handle password resets by generating a random token, storing its hash with a 15-min expiry, emailing a link with the raw token, verifying the hash on reset, hashing the new password, and invalidating the token. Hash stored tokens. Rate limit. Do not reveal if the email exists. Notify the user after reset.
Generate a random token, store its hash with a 15-min expiry on the user, email a link with the raw token, verify the hash on reset, hash the new password, invalidate the token, and notify the user. Rate limit the request endpoint.
If the database is leaked, attackers cannot use the hashes to reset passwords. The raw token is in the email link, not the database. Compare hashes on reset.
15 minutes is good. Do not use 24 hours. Short expiry limits the window an attacker has to use a stolen token. If the user needs more time, they can request another.
To prevent email enumeration. If an attacker can tell which emails are registered, they can target those accounts. Return the same response whether the email is registered or not: 'If the email exists, a reset link has been sent.'
Invalidate the token (one-time use), hash the new password, notify the user by email that the password was changed, and invalidate all existing JWTs (e.g., bump a tokenVersion field) so attackers with old tokens are logged out.
Ready to master Node.js completely?
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course to dive deeper with high-quality video tutorials, solve interview questions, and a premium community.
Master Node.js
Want to upskill yourself, crack your next interview, and get your dream job? Join our comprehensive course.

