Authentication45-60 min
Password Reset Flow Implementation
This guide covers reset token creation, hashing reset tokens, generic responses, email links, form validation, and single-use token invalidation.
Node.jsExpressReactEmail
Prerequisites
- A users table with email and password hash fields.
- A mail sending utility already configured.
- A frontend route for entering a new password.
- A database table or columns for password reset tokens.
1
Create a reset token table
Implementation snippet
CREATE TABLE password_reset_tokens (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users(id),
token_hash TEXT NOT NULL UNIQUE,
expires_at TIMESTAMP NOT NULL,
used_at TIMESTAMP,
created_at TIMESTAMP NOT NULL DEFAULT now()
);2
Create the forgot-password endpoint
- Accept an email address.
- Always return a generic success response even if no account exists.
- Generate a random token only when the user exists.
- Hash the token before storing it.
- Email the raw token inside a reset URL.
Implementation snippet
import crypto from "node:crypto";
app.post("/auth/forgot-password", async (req, res) => {
const user = await users.findByEmail(req.body.email);
if (user) {
const token = crypto.randomBytes(32).toString("hex");
const tokenHash = crypto.createHash("sha256").update(token).digest("hex");
await passwordResets.create({
userId: user.id,
tokenHash,
expiresAt: new Date(Date.now() + 30 * 60 * 1000),
});
await sendPasswordResetEmail(user.email, `${process.env.APP_URL}/reset-password?token=${token}`);
}
res.json({ message: "If that email exists, a reset link has been sent" });
});3
Reset the password
- Read the reset token and new password from the request.
- Hash the submitted token and find an unused, unexpired row.
- Hash the new password using bcrypt or argon2.
- Update the user's password hash.
- Mark the token as used in the same transaction.
Implementation snippet
app.post("/auth/reset-password", async (req, res) => {
const tokenHash = crypto.createHash("sha256").update(req.body.token).digest("hex");
const reset = await passwordResets.findValidToken(tokenHash);
if (!reset) return res.status(400).json({ message: "Reset link is invalid or expired" });
const passwordHash = await bcrypt.hash(req.body.password, 12);
await db.transaction(async (tx) => {
await users.updatePassword(reset.userId, passwordHash, tx);
await passwordResets.markUsed(reset.id, tx);
});
res.json({ message: "Password updated" });
});4
Build the reset form
- Read token from the URL query string.
- Ask the user for password and confirmation.
- Validate minimum length and matching confirmation before calling the API.
- Show a clear expired-link message if the API returns 400.
- Redirect to login after a successful reset.
5
Security checklist
Checklist
- Reset tokens are random, single-use, and expire quickly.
- Raw tokens are never stored in the database.
- Forgot-password responses do not reveal whether an email exists.
- Old sessions are invalidated after password reset if your app supports session tracking.
- Rate limiting is applied to forgot-password requests.