Our website is made possible by displaying online advertisements to our visitors. Please consider supporting us by disabling your ad blocker.

Hash Password Data In MongoDB With Mongoose And Bcrypt

TwitterFacebookRedditLinkedInHacker News

When creating a web application that handles user information it is a good idea to protect anything considered sensitive rather than storing it as plaintext within a database. The goal is to make it as difficult as possible for a malicious person to obtain access to this sensitive information. Rather than encrypting sensitive information with the knowledge that it can one day become decrypted, it is better to hash this sensitive data instead because hashing is a one-way process.

In this tutorial we’re going to take a look at hashing password data with bcryptjs before storing it in a MongoDB NoSQL database with Mongoose and Node.js.

A while back I had written a similar tutorial titled, Hashing Passwords Stored in Couchbase Server with Node.js, which focused on Couchbase Server, but a lot of the concepts carry over. We’re just using a different NoSQL database with an ODM this time around.

Creating a New Node.js Project with Express.js and MongoDB Support

To get started, we’re going to create a new Node.js project. The assumption is that you already have a MongoDB instance to play around with. If you need help getting started with installing or deploying MongoDB, you might check out my previous tutorial titled Getting Started with MongoDB as A Docker Container Deployment.

From the command line, execute the following:

npm init -y
npm install express body-parser mongoose bcryptjs --save

The above commands will create a new package.json file and install each of our project dependencies. We’ll be using express and body-parser for creating our API, mongoose for interacting with MongoDB and bcryptjs for hashing and comparing our sensitive data.

To get us started with our project, we want to add some boilerplate code. Create an app.js file with the following JavaScript code:

const Express = require("express");
const BodyParser = require("body-parser");
const Mongoose = require("mongoose");
const Bcrypt = require("bcryptjs");

var app = Express();

app.use(BodyParser.json());
app.use(BodyParser.urlencoded({ extend: true }));

Mongoose.connect("mongodb://localhost/thepolyglotdeveloper");

const UserModel = new Mongoose.model("user", {
    username: String,
    password: String
});

app.post("/register", async (request, response) => {});
app.post("/login", async (request, response) => {});

app.get("/dump", async (request, response) => {
    try {
        var result = await UserModel.find().exec();
        response.send(result);
    } catch (error) {
        response.status(500).send(error);
    }
});

app.listen(3000, () => {
    console.log("Listening at :3000...");
});

I had already discussed the development of a REST API with Node.js and MongoDB in a previous tutorial, so we won’t get into all the details when it comes to configuring MongoDB and Express.js in our project. What we care about is the register, login, and dump endpoints.

We’ll be discussing the register and login endpoints shortly, but the dump endpoint will allow us to see what’s in our collection as defined by the UserModel data model.

Hashing and Validating Passwords for Safe Storage in the MongoDB NoSQL Database

With the boilerplate code out of the way, we can take a look at actually hashing our data and then comparing it to validate it. We’re going to use the typical registration and login scenario. Passwords are always sensitive so we must hash them and when we log in, we need to validate our plaintext request data with the hashed data that is stored.

Take a look at the following register endpoint function:

app.post("/register", async (request, response) => {
    try {
        request.body.password = Bcrypt.hashSync(request.body.password, 10);
        var user = new UserModel(request.body);
        var result = await user.save();
        response.send(result);
    } catch (error) {
        response.status(500).send(error);
    }
});

In the event that the client calls the above endpoint we first hash the password using Bcrypt. After hashing the password we can continue to create a new instance of our data model and save it to the database. The saved password will not be plaintext.

When the user wishes to sign in, we can proceed to doing some validation:

app.post("/login", async (request, response) => {
    try {
        var user = await UserModel.findOne({ username: request.body.username }).exec();
        if(!user) {
            return response.status(400).send({ message: "The username does not exist" });
        }
        if(!Bcrypt.compareSync(request.body.password, user.password)) {
            return response.status(400).send({ message: "The password is invalid" });
        }
        response.send({ message: "The username and password combination is correct!" });
    } catch (error) {
        response.status(500).send(error);
    }
});

When the client reaches the above endpoint we first attempt to find the user based on the username that was provided in the request. If the user was found, we attempt to compare the provided plaintext password against the stored hashed password. If the password data compares correctly then we can tell the client that they are successful.

What we did works well, but it can be simplified a bit and better prepared for the future.

Developing Custom Functions and Preprocessing Functions for Mongoose

With Mongoose we have the ability to do a sense of preprocessing on any of the functions that Mongoose has built in. We also have the opportunity to create our own methods for our models.

We’re going to prepare our code to perform actions every time the save method is called on a model and build our own compare password function on the model.

Let’s go back to our boilerplate code and make it look like the following:

const Express = require("express");
const BodyParser = require("body-parser");
const Mongoose = require("mongoose");
const Bcrypt = require("bcryptjs");

var app = Express();

app.use(BodyParser.json());
app.use(BodyParser.urlencoded({ extend: true }));

Mongoose.connect("mongodb://localhost/thepolyglotdeveloper");

const UserSchema = new Mongoose.Schema({
    username: String,
    password: String
});

UserSchema.pre("save", function(next) {});

UserSchema.methods.comparePassword = function(plaintext, callback) {};

const UserModel = new Mongoose.model("user", UserSchema);

app.post("/register", async (request, response) => {});
app.post("/login", async (request, response) => {});

app.get("/dump", async (request, response) => {
    try {
        var result = await UserModel.find().exec();
        response.send(result);
    } catch (error) {
        response.status(500).send(error);
    }
});

app.listen(3000, () => {
    console.log("Listening at :3000...");
});

So we’re looking at a few differences in the above code. Now we are specifically defining a schema and defining a pre event for the save function as well as creating a comparePassword function.

Let’s first look at the pre event:

UserSchema.pre("save", function(next) {
    if(!this.isModified("password")) {
        return next();
    }
    this.password = Bcrypt.hashSync(this.password, 10);
    next();
});

When the save function is called, we will first check to see if the user is being created or changed. If the user is not being created or changed, we will skip over the hashing part. We don’t want to hash our already hashed data.

Now we can look at the comparePassword function:

UserSchema.methods.comparePassword = function(plaintext, callback) {
    return callback(null, Bcrypt.compareSync(plaintext, this.password));
};

The above function will take some provided data, compare it against our hashed data, and then return the boolean response within the callback.

With the functions in place, we can now take a look at our original register and login endpoints, starting with the register endpoint:

app.post("/register", async (request, response) => {
    try {
        var user = new UserModel(request.body);
        var result = await user.save();
        response.send(result);
    } catch (error) {
        response.status(500).send(error);
    }
});

Notice that in our endpoint function we are no longer directly working with the Bcrypt hashing. Remember, this is all taken care of for us prior to saving. In the login endpoint we have the following:

app.post("/login", async (request, response) => {
    try {
        var user = await UserModel.findOne({ username: request.body.username }).exec();
        if(!user) {
            return response.status(400).send({ message: "The username does not exist" });
        }
        user.comparePassword(request.body.password, (error, match) => {
            if(!match) {
                return response.status(400).send({ message: "The password is invalid" });
            }
        });
        response.send({ message: "The username and password combination is correct!" });
    } catch (error) {
        response.status(500).send(error);
    }
});

The above endpoint didn’t really change much in size. However, you might consider it a lot cleaner, more specifically if there are other reasons why you’d need to hash or validate password information.

The above solution with the pre and comparePassword functions was inspired from a solution I found on Stack Overflow. I just wanted to give credit where credit was due, even though my example is a little different.

Conclusion

You just saw how to safely store the password data and other sensitive data for your API by hashing it with Bcrypt. This example used Node.js, Mongoose, and MongoDB while my previous example used Couchbase, both of which are NoSQL databases.

If you’re interested in learning more about developing REST APIs with MongoDB, you should check out my previous tutorial on the subject. For even more information with hands on examples, you can check out my eBook and video course titled, Web Services for the JavaScript Developer.

Nic Raboy

Nic Raboy

Nic Raboy is an advocate of modern web and mobile development technologies. He has experience in C#, JavaScript, Golang and a variety of frameworks such as Angular, NativeScript, and Unity. Nic writes about his development experiences related to making web and mobile development easier to understand.