Skip to content
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

  1. Accept an email address.
  2. Always return a generic success response even if no account exists.
  3. Generate a random token only when the user exists.
  4. Hash the token before storing it.
  5. 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

  1. Read the reset token and new password from the request.
  2. Hash the submitted token and find an unused, unexpired row.
  3. Hash the new password using bcrypt or argon2.
  4. Update the user's password hash.
  5. 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

  1. Read token from the URL query string.
  2. Ask the user for password and confirmation.
  3. Validate minimum length and matching confirmation before calling the API.
  4. Show a clear expired-link message if the API returns 400.
  5. 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.

Need implementation help?

Want this built correctly in your codebase?

Send us your stack, repo context, and the feature you need. We will help you implement it cleanly and hand over the working code.

Free scoping callFixed timelineFull source ownership
Get implementation help