Build forgot and reset password functionalities using Express.js

First, install jsonwebtoken, @sendgrid/mail, and bcrypt as dependencies, and @types/jsonwebtoken and @types/bcrypt as dev dependencies in your Express.js project. We will use these packages to create secure tokens, send emails with password reset links, and create password hashes respectively.

Create forgot password route

With the following code, let's create a forgot-password route that takes the user's email, checks if the user exists in the database, and sends him/her a password reset link.

import mail from '@sendgrid/mail';
import jwt from 'jsonwebtoken';
import User from '../models/user';
import { passwordResetTemplate } from '../lib/emailTemplates';

router.post('/forgot-password', async (req, res) => {
  const { email } = req.body;

  if (!email) {
    console.log('Email is required');
    res.status(400);
    throw new Error('Email is required');
  }

  try {
    const user = await User.findOne({ email }).orFail();
    const token = jwt.sign(
      { password: user.password },
      process.env.JWT_SECRET as string,
      { expiresIn: '15m' }
    );
    const link = `${process.env.CLIENT_URL}/reset-password/${user._id}/${token}`;

    await mail.send(passwordResetTemplate(user.toObject(), link));
    res.status(200).json('Password reset details sent to your email');
  } catch (err) {
    console.log(err);
    throw err;
  }
});

In the above code, first, we get the user's email from the req.body. We check for the email and throw an error if no email is provided. After that, we query the database to find the user with the provided email.

If the user exists, we use the jwt.sign method to create a token with the user's password hash. We do this to keep track of the password the user is trying to reset and restrict him/her from resetting the password more than once using the same link. We also set the token expiry so the user must reset the password within 15 minutes of the request.

Next, we construct a link with the user's _id and the token. This is the frontend URL where the user will be navigated to input the new password and perform the reset. Make sure a corresponding page to this link exists in your frontend application.

Finally, we send an email using the SendGrid mail.send method with the password reset link.

If you are interested in learning about sending emails using SendGrid and Next.js or Express.js, I have an article on Send emails using SendGrid from Next.js and Express.js applications that provides further insights.

Create reset password route

Now, to handle the creation of a new password, let's create a reset-password route with the following code:

import bcrypt from 'bcrypt';
import mail from '@sendgrid/mail';
import User from '../models/user';
import jwt, { JwtPayload } from 'jsonwebtoken';
import { passwordResetConfirmationTemplate } from '../lib/emailTemplates';

router.patch('/reset-password/:userId/:token', async (req, res) => {
  const { password } = req.body;
  if (!password) {
    console.log('Password is required');
    res.status(400);
    throw new Error('Password is required');
  }

  const { userId, token } = req.params;
  try {
    const user = await User.findById(userId).orFail();
    const decoded = jwt.verify(
      token,
      process.env.JWT_SECRET as string
    ) as JwtPayload;
    if (decoded.password !== user.password) {
      console.log('Invalid token');
      res.status(400);
      throw new Error('Invalid token');
    }
    const salt = await bcrypt.genSalt(10);
    const hashedPassword = await bcrypt.hash(password, salt);

    await User.findOneAndUpdate(
      { _id: userId },
      {
        password: hashedPassword,
      }
    ).orFail();
    await mail.send(passwordResetConfirmationTemplate(user.toObject()));
    res.status(201).json('Password reset successful');
  } catch (err) {
    console.log(err);
    throw err;
  }
});

In the above code, first, we get the password from the req.body. We check for the password and throw an error if no password is provided. After that, we get the userId and token from the req.params and query the database to find the user with the provided ID.

If the user exists, we decode the token and create a decoded variable by using the jwt.verify method. The decoded variable is the object that we stored in the token when signed from the forgot-password route. Now, we check if the password hash stored in the decoded object matches with the hash stored in the database. If it doesn't, we throw an error.

Otherwise, we create salt using the bcrypt.genSalt method, hash the password by using the bcrypt.hash method, update the user in the database with the new hash, and send a confirmation email saying the password reset is successful.

By the way, if you are interested in learning about creating secure authentication using HTTP-only cookies, JSON Web Tokens, and Express.js, I have an article on Create secure authentication using HTTP-only cookies in Express.js that provides further insights.

Behind the scenes

So, for the first request made to the reset-password route, the password hash stored in the decoded object will be the same as the hash stored in the database. Consequently, it will pass the check, and the password reset will be successful as long as the request is made within 15 minutes.

However, on a successful password reset, a new hash will be saved to the database. Any subsequent request made to the reset-password route using the same link, the password hash stored in the decoded object won't match with the hash stored in the database. This will fail the check and throw an error, preventing the user from resetting the password again.

That's it! You can now use these endpoints in your frontend application to build the forgot and reset password functionalities. Hope you found the article useful. Feel free to share the article with others who might find it useful as well. Cheers!

Share this article with your friends

Copy URL

Elevate your JavaScript and freelance journey

Supercharge your JavaScript skills and freelance career. Subscribe now for expert tips and insights!