Eze Sunday Eze Sunday is a full-stack software developer and technical writer passionate about solving problems one line of code at a time. Currently building Raveshift.com, a crypto Exchange and tools for crypto payment solutions.

Implementing a secure password reset in Node.js

8 min read 2473

Node.js Logo

Creating a strong password that you can actually remember isn’t easy. Users who use unique passwords for different websites (which is ideal) are more likely to forget their passwords. So, it’s crucial to add a feature that allows your users to securely reset their password when they forget it.

This article is about how you can build a secure reset password feature with Node.js and Express.

First off, to follow along with this tutorial, here are some requirements to note:

  • You should have a basic understanding of Javascript and Node.js
  • You have NodeJS installed or you can download and install the latest version of Node.js
  • You have MongoDB database installed or can download and install MongoDB database. Another option is to create an account on the MongoDB website and set up a free database

Now, let’s get started.

Creating a workflow for your password reset

The workflow for your reset password can take different shapes and sizes, depending on how usable and secure you want your application to be.

In this article, we’ll implement a standard secured reset password design. The diagram below shows how the workflow for the feature will work.

  • Request password reset if the user exists
  • Validate user information
  • Provide identification information
  • Reset password or reject password verification request
    Reset Password Workflow

Building a project for password reset

Let’s create a simple project to demonstrate how the password reset feature can be implemented. Note that you can find the completed project on password reset with Node.js on GitHub, or you can also jump to the password reset section of this tutorial.

Let’s first initialize our project with the npm package manager.

Run npm init on your Terminal/Command Prompt and follow the instructions to set up the project.

Folder structure and files within your project

We made a custom demo for .
No really. Click here to check it out.

Our folder structure will look like this:

  • Controllers
    • auth.controller.js
  • Services
    • auth.service.js
  • Models
    • user.model.js
    • token.model.js
  • Routes
    • index.route.js
  • Utils
    • Emails
      • template
        • requestResetPassword.handlebars
        • resetPassword.handlebars
      • sendEmail.js
  • index.js
  • db.js
  • package.json

Dependencies for setting up your project with password reset

Run the code below to install the dependencies we’ll use in this project with npm.

npm install bcrypt, cors, dotenv, express, express-async-errors, handlebars, jsonwebtoken, mongoose, nodemailer, nodemon

We’ll use:

  • bcrypt to hash passwords and reset tokens
  • cors to disable Cross-origin Resource Sharing at least for development purposes
  • dotenv to allow our node process to have access to the environment variables
  • express-async-errors to catch all async errors so our entire codebase doesn’t get littered with try-catch
  • handlebars as a templating engine to send HTML emails
  • mongoose to as a driver to interface with MongoDB database
  • nodemailer to send emails
  • nodemon to restart the server when a file changes

Setup environment variables

Some variables will vary in our application depending on the environment we are running our application in. For these variables, we’ll add them to our environment variables.

Create an .env file in your root directory and paste the following variables inside. We’re adding a bcrypt salt, our database url, a JWT_SECRET and a client url. You’ll see how we’ll use them as we proceed.

BCRYPT_SALT=10
DB_URL=mongodb://127.0.0.1:27017/testDB
JWT_SECRET=mfefkuhio3k2rjkofn2mbikbkwjhnkj
CLIENT_URL=localhost://8090

Connecting to the MongoDB database

Let’s create a connection to our MongoDB. This code should be in your db.js file in the root directory.

db.js

const mongoose = require("mongoose");
let DB_URL = process.env.DB_URL;// here we are using the MongoDB Url we defined in our ENV file

module.exports = async function connection() {
  try {
    await mongoose.connect(
      DB_URL,
      {
        useNewUrlParser: true,
        useUnifiedTopology: true,
        useFindAndModify: false,
        useCreateIndex: true,
        autoIndex: true,
      },
      (error) => {
        if (error) return new Error("Failed to connect to database");
        console.log("connected");
      }
    );
  } catch (error) {
    console.log(error);
  }
};

Setting up the Express app

Let’s build our application entry point and serve it at port 8080. Copy and paste this into your index.js file in the root directory.

require("express-async-errors");
require("dotenv").config();
const express = require("express");
const app = express();
const connection = require("./db");
const cors = require("cors");

const port = 8080;
(async function db() {
  await connection();
})();

app.use(cors());
app.use(express.json());
app.use("/api/v1", require("./routes/index.route"));
app.use((error, req, res, next) => {
  res.status(500).json({ error: error.message });
});

app.listen(port, () => {
  console.log("Listening to Port ", port);
});

module.exports = app;

Setting up token and user models

We’ll need to create a User and Token model to set up our password reset system.

1. Token model

Our token model will have an expiry time of about 1 hour. Within this period, the user is expected to complete the password reset process, otherwise the token will be deleted from the database. With MongoDB, you don’t have to write additional code to make this work. Just set expires in your date field, like so:

  createdAt: {
    type: Date,
    default: Date.now,
    expires: 3600,// this is the expiry time
  },

Here is what our model will look like.

You should add this code to the file token.model.js inside the model directory.

models/token.model.js

const mongoose = require("mongoose");
const Schema = mongoose.Schema;
const tokenSchema = new Schema({
  userId: {
    type: Schema.Types.ObjectId,
    required: true,
    ref: "user",
  },
  token: {
    type: String,
    required: true,
  },
  createdAt: {
    type: Date,
    default: Date.now,
    expires: 3600,// this is the expiry time in seconds
  },
});
module.exports = mongoose.model("Token", tokenSchema);

2. User model

The user model will describe how the user data will be saved.

And, of course, before discuss creating a secure reset password mechanism, we need to ensure we are creating and storing a secure password in the first place.

Keep in mind that storing passwords in plain text is bad practice and should never be done. Instead, use a secure and strong one-way algorithm to hash and store passwords, preferably one with a salt. In our case, we’ll be using bcrypt, as shown in the code below.

We’ll use bcrypt to hash passwords so that even we as administrators wouldn’t know the passwords — you shouldn’t know your user’s passwords, and it will be almost impossible to figure out the reverse in case the database is hijacked.

const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const Schema = mongoose.Schema;
const bcryptSalt = process.env.BCRYPT_SALT;
const userSchema = new Schema(
  {
    name: {
      type: String,
      trim: true,
      required: true,
      unique: true,
    },
    email: {
      type: String,
      trim: true,
      unique: true,
      required: true,
    },
    password: { type: String },
  },
  {
    timestamps: true,
  }
);
userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) {
    return next();
  }
  const hash = await bcrypt.hash(this.password, Number(bcryptSalt));
  this.password = hash;
  next();
});
module.exports = mongoose.model("user", userSchema);

Before the password is saved, we use the pre-save MongoDB hook to hash the password first in this part of the code. We are using a salt of 10, as shown in our .env file, because this is strong enough to reduce the possibility of bad guys guessing users’ passwords.

userSchema.pre("save", async function (next) {
  if (!this.isModified("password")) {
    return next();
  }
  const hash = await bcrypt.hash(this.password, Number(bcryptSalt));
  this.password = hash;
  next();
});

Create services for the password reset process

We’ll have three services to completely start and process our password reset process.

1. Sign-up service

Of course, we can’t reset a user’s password if they don’t have an account. So, this service will create an account for a new user.

The code below should be in the service/auth.service.js file:

const JWT = require("jsonwebtoken");
const User = require("../models/User.model");
const Token = require("../models/Token.model");
const sendEmail = require("../utils/email/sendEmail");
const crypto = require("crypto");
const bcrypt = require("bcrypt");

const signup = async (data) => {
  let user = await User.findOne({ email: data.email });
  if (user) {
    throw new Error("Email already exist");
  }
  user = new User(data);
  const token = JWT.sign({ id: user._id }, JWTSecret);
  await user.save();
  return (data = {
    userId: user._id,
    email: user.email,
    name: user.name,
    token: token,
  });
};

2. Password reset request service

Following our workflow, the first step is to request a password reset token, right?

So, let’s start there. In the code below, we check if the user exists. If the user exists, then we check if there is an existing token that has been created for this user. If one exists, we delete the token.

  const user = await User.findOne({ email });

  if (!user) {
      throw new Error("User does not exist");
  }
  let token = await Token.findOne({ userId: user._id });
  if (token) { 
        await token.deleteOne()
  };

Now, pay attention to this part: we are going to create a new random token using the Node.js crypto API. This is the token we’ll send to the user:

let resetToken = crypto.randomBytes(32).toString("hex");

Now, create a hash of this token, which we’ll save in the database because saving plain resetToken in our database is the same as saving passwords in plain text, and that will defeat the entire purpose of setting up a secure password reset.

However, we’ll send the plain token to the users’ email as shown in the code below, and then in the next section, we’ll verify the token and allow them to create a new password.

service/auth.service.js

const JWT = require("jsonwebtoken");
const User = require("../models/User.model");
const Token = require("../models/Token.model");
const sendEmail = require("../utils/email/sendEmail");
const crypto = require("crypto");
const bcrypt = require("bcrypt");

const requestPasswordReset = async (email) => {

  const user = await User.findOne({ email });

  if (!user) throw new Error("User does not exist");
  let token = await Token.findOne({ userId: user._id });
  if (token) await token.deleteOne();
  let resetToken = crypto.randomBytes(32).toString("hex");
  const hash = await bcrypt.hash(resetToken, Number(bcryptSalt));

  await new Token({
    userId: user._id,
    token: hash,
    createdAt: Date.now(),
  }).save();

  const link = `${clientURL}/passwordReset?token=${resetToken}&id=${user._id}`;
  sendEmail(user.email,"Password Reset Request",{name: user.name,link: link,},"./template/requestResetPassword.handlebars");
  return link;
};

The reset password link contains the token and the user ID, both of which will be used to verify the user’s identity before they can reset their password.

It also contains the client url. The root domain the user will have to click to continue the reset process.

const link = `${clientURL}/passwordReset?token=${resetToken}&id=${user._id}`;

You can find the function sendEmail in the GitHub repository here and the email template here. We are using nodemailer and handlebars templating engine to send the email.

1. Reset password service

Oh, look! I received an email to confirm I made the password reset request. Now I’m going to click on the link next.

Email Confirmation for Password Reset

Once you click on the link, it should take you to the password reset page. You can build that frontend with any technology you desire, as this is just an API.

At this point, we’ll send back the token, a new password, and a user id to verify the user and create a new password afterward.

Here’s what we’ll be sending back to the reset password API will look like:

{
    "token":"4f546b55258a10288c7e28650fbebcc51d1252b2a69823f8cd1c65144c69664e",
    "userId":"600205cc5fdfce952e9813d8",
    "password":"kjgjgkflgk.hlkhol"
}

It will be processed by the code below.

service/auth.service.js

const resetPassword = async (userId, token, password) => {
  let passwordResetToken = await Token.findOne({ userId });
  if (!passwordResetToken) {
    throw new Error("Invalid or expired password reset token");
  }
  const isValid = await bcrypt.compare(token, passwordResetToken.token);
  if (!isValid) {
    throw new Error("Invalid or expired password reset token");
  }
  const hash = await bcrypt.hash(password, Number(bcryptSalt));
  await User.updateOne(
    { _id: userId },
    { $set: { password: hash } },
    { new: true }
  );
  const user = await User.findById({ _id: userId });
  sendEmail(
    user.email,
    "Password Reset Successfully",
    {
      name: user.name,
    },
    "./template/resetPassword.handlebars"
  );
  await passwordResetToken.deleteOne();
  return true;
};

Notice how we are using bcrypt to compare the token the server received with the hashed version in the database?

  const isValid = await bcrypt.compare(token, passwordResetToken.token);

If they are the same, then we go ahead to hash the new password…

  const hash = await bcrypt.hash(password, Number(bcryptSalt));

…and update the user’s account with the new password.

  await User.updateOne(
    { _id: userId },
    { $set: { password: hash } },
    { new: true }
  );

Just so we are keeping the user informed with every step, we’ll send them an email to confirm the change in password and delete the token from the database.

So far, we’ve been able to create the services to sign up a new user and to reset their password.

Bravo.

Controllers for password reset services

Here are the controllers for each of those services. The controllers collect data from the user, send them to the services to process the data, and then return the result back to the user.

controllers/auth.controller.js

const {
  signup,
  requestPasswordReset,
  resetPassword,
} = require("../services/auth.service");

const signUpController = async (req, res, next) => {
  const signupService = await signup(req.body);
  return res.json(signupService);
};

const resetPasswordRequestController = async (req, res, next) => {
  const requestPasswordResetService = await requestPasswordReset(
    req.body.email
  );
  return res.json(requestPasswordResetService);
};

const resetPasswordController = async (req, res, next) => {
  const resetPasswordService = await resetPassword(
    req.body.userId,
    req.body.token,
    req.body.password
  );
  return res.json(resetPasswordService);
};

module.exports = {
  signUpController,
  resetPasswordRequestController,
  resetPasswordController,
};

Congratulations, you’ve created a secure reset password feature for your users.

What can you do next?

Here are a few things you could do to make this password reset feature more secure.

  • You could ask users to provide answers to their security questions (they must have created these questions and answers already).
  • Use secure hosting services. An insecure hosting service can create a backdoor for you as administrator. If your hosting service isn’t getting it right, the bad guys can compromise your own system even if your system seems to be safe.
  • Implement Two-Factor Authentication (2FA) with Google Authenticator or SMS. This way, even if the user’s email has been compromised, the hacker will also need to have the user’s phone.
  • Don’t store users’ passwords at all. It’s stressful having to reset your password each time you forget it. Instead, you could allow your users to log in with already existing services like Facebook, Github, GMail, and the like. That way, they won’t need to remember their password ever again. They will only need to worry about their passwords on those third-party websites.

If you want to play around with the completed project, you can check it out on GitHub.

Conclusion

Digital security isn’t a joke. Hackers are always on the lookout for new ways to hack systems, so you should always be on the lookout for new ways to make life difficult for them.

Your users trust you to secure their data and you don’t want to disappoint them. What other ways would you recommend to make a password reset feature more secure? Thanks for reading.

200’s only Monitor failed and slow network requests in production

Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third party services are successful, try LogRocket. https://logrocket.com/signup/

LogRocket is like a DVR for web apps, recording literally everything that happens on your site. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.

LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. .
Eze Sunday Eze Sunday is a full-stack software developer and technical writer passionate about solving problems one line of code at a time. Currently building Raveshift.com, a crypto Exchange and tools for crypto payment solutions.

Leave a Reply