← coderrocketfuel.com

Store Passwords In MongoDB With Node.js, Mongoose, & Bcrypt

When building applications with Node.js, MongoDB, and Mongoose, there will be times where you'll need to handle user passwords.

Since it's a big no-no to store passwords as plain-text in your database, how are you supposed to handle and store them?

The answer is to create what's called a hash of the password. This is a long, complex, and unique string that is created using the plain-text version of the user's password. Instead of storing the user's plain-text password in the database, you'll store the hash version instead.

If your database gets hacked in this case, the hacker will be left with random hash strings instead of plain text ones they could use to easily exploit your user's accounts.

The only way to find the password or message that produces a given hash is to attempt a brute-force-search of possible inputs to see if they produce a match. Or use a rainbow table of matched hashes. If you generate your hashes correctly, both methods would take a hacker lots of time and tons of computing power to complete (which costs money).

In this guide, we'll go over how to do the following:

  • Store a hashed password in a MongoDB database using Mongoose. To create the hashed password, we'll use the Node.js implementation of Bcrypt called bcrypt.js.
  • Create a Mongoose method that checks if a user-inputted password matches the hash that's stored in the database. This will simulate what would happen on a login page of your website.

Before moving forward, we'll assume that you have Node.js installed on your machine and a MongoDB database to work with that is connected to your Node.js application.

If needed, we wrote a guide on installing Node.js and another guide on how to create and connect to a MongoDB Atlas database with Node.js.

Let's get started!

Table Of Contents

Install The Bcrypt.js NPM Package

Before we can store a hashed password in the database, we need to install the bcrypt.js NPM package.

Bcrypt is one of the most used encryption libraries today. It incorporates hash encryption along with a work factor, which allows you to determine how expensive the hash function will be (i.e. how long it takes to decrypt it by brute force measures). Therefore, it keeps up with Moore's law, so as computers get faster you can increase the work factor and the hash will get slower to brute force.

You can install it with one of the following commands:

NPM:

npm install bcryptjs --save

Yarn:

yarn add bcryptjs

When that's done installing, we're ready for the next section!

Store A Hashed Password In The Database

Now we can hash a password and store it in the database.

The Hashing Function

First, let's go over the basic function that will take a text password and create a hash.

The full code is below:

const bcrypt = require("bcryptjs")

const password = "mypass123"
const saltRounds = 10

bcrypt.genSalt(saltRounds, function (saltError, salt) {
  if (saltError) {
    throw saltError
  } else {
    bcrypt.hash(password, salt, function(hashError, hash) {
      if (hashError) {
        throw hashError
      } else {
        console.log(hash)
        //$2a$10$FEBywZh8u9M0Cec/0mWep.1kXrwKeiWDba6tdKvDfEBjyePJnDT7K
      }
    })
  }
})

First, we create two variables named password and saltRounds.

The password variable will be the string bcrypt hashes. For example, this would be the string inputted by a user via a signup form.

And the saltRounds integer gives us control over what the computing cost of processing the data will be. The higher the number, the longer it will take a machine to calculate the hash associated with the password.

So, we want to choose a number that is both high enough to make a brute force attack take too long and short enough to keep the end user's patience from ending when signing up or logging into their account.

Then, we generate a salt using our saltRounds integer:

bcrypt.genSalt(saltRounds, function (saltError, salt) {
  if (saltError) {
    throw saltError
  } else {

    . . .

  }
})

The bcrypt.genSalt() method takes our saltRounds integer of 10 as a parameter and returns a callback function with the generated salt result included.

Using the generated salt, we can then create the hash string:

bcrypt.hash(password, salt, function(hashError, hash) {
  if (hashError) {
    throw hashError
  } else {
    console.log(hash)
    //$2a$10$FEBywZh8u9M0Cec/0mWep.1kXrwKeiWDba6tdKvDfEBjyePJnDT7K
  }
})

This bcrypt.hash() method takes the "mypass123" string (would be the password value inputted by a user) and the salt we generated as parameters. In the callback() function, it logs the generated hash string for our password.

When you run the code, the hash should be printed to your command line and look similar to this:

$2a$10$FEBywZh8u9M0Cec/0mWep.1kXrwKeiWDba6tdKvDfEBjyePJnDT7K

This is the string you would store in your database instead of the password in plain-text form.

Use Mongoose To Save The Hashed Password To The Database

Now that we know how to create a hashed password, let's go over how to add it to the database using Mongoose.

If you don't already have Mongoose installed, you can do so with one of the commands below.

NPM:

npm install mongoose --save

Yarn:

yarn add mongoose

As an example, we'll use a Mongoose model that looks like the following:

const mongoose = require("mongoose")
const bcrypt = require("bcryptjs")

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

module.exports = mongoose.model("User", UserSchema)

First, we create what's called a Mongoose Schema with the new mongoose.Schema() class. Each key in the UserSchema code defines a property in the documents that will be added to the MongoDB database:

  • username: the unique identifier given to each user.
  • password: the salted and hashed representation of the user's password.

To use the schema definition, we need to convert the UserSchema variable into a Mongoose model we can work with. To do that, we pass it into the mongoose.model("User", UserSchema) method. We then export the model so we can require() and use it in other files where we interact with the database.

Whenever a new user is added to the database, this model will be used to tell Mongoose what kind of data needs to be included.

Next, we need to add a Mongoose pre middleware function to the model we just created. This middleware needs to salt and hash passwords before they are saved to the database.

It should look like the following:

UserSchema.pre("save", function (next) {
  const user = this

  if (this.isModified("password") || this.isNew) {
    bcrypt.genSalt(10, function (saltError, salt) {
      if (saltError) {
        return next(saltError)
      } else {
        bcrypt.hash(user.password, salt, function(hashError, hash) {
          if (hashError) {
            return next(hashError)
          }

          user.password = hash
          next()
        })
      }
    })
  } else {
    return next()
  }
})

This is a Mongoose pre middleware function that will be called before any user document is saved or changed. And has the overall purpose of hashing the password whenever a user document is saved to the database with a new password value.

Inside the pre middleware function, the first thing we do is check whether or not our function needs to hash a password via the (this.isModified("password") || this.isNew) code.

This means that our function needs to hash the password for the document if the "password" value has been changed (i.e. user changed their password) or an entirely new document is being added to the database (i.e. a new user has signed up). Otherwise, the pre function doesn't need to do any hashing.

Then, we use the bcrypt.hash() method to generate the hash for the password. It takes the password and the salt we generated as parameters. In the callback function, it returns the generated hash string for our password.

When your password is saved to the database, the hash string will look something like this:

$2a$10$FEBywZh8u9M0Cec/0mWep.1kXrwKeiWDba6tdKvDfEBjyePJnDT7K

For your reference, the code for the entire example Mongoose model should look like this:

const mongoose = require("mongoose")
const bcrypt = require("bcryptjs")

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

UserSchema.pre("save", function (next) {
  const user = this

  if (this.isModified("password") || this.isNew) {
    bcrypt.genSalt(10, function (saltError, salt) {
      if (saltError) {
        return next(saltError)
      } else {
        bcrypt.hash(user.password, salt, function(hashError, hash) {
          if (hashError) {
            return next(hashError)
          }

          user.password = hash
          next()
        })
      }
    })
  } else {
    return next()
  }
})

module.exports = mongoose.model("User", UserSchema)

The model would now be ready to use when adding a user's hashed password to the database.

Use The Mongoose Model Hashing Method

As an example, let's go over how the Mongoose model would be used in practice.

Let's create an imaginary scenario where you have a signup form on your website that has both the username and password input fields.

When that user submits the field, you want to create a new user in the database with the user's username and hashed password.

The code for achieving that could look like the following:

const UserModel = require("./models/user.js")

module.exports = {
  createANewUser: function(username, password, callback) {
    const newUserDbDocument = new UserModel({
      username: username,
      password: password
    })

    newUserDbDocument.save(function(error) {
      if (error) {
        callback({error: true})
      } else {
        callback({success: true})
      }
    })
  }
}

This code creates a basic function called createANewUser() that is exported using the module.exports object.

In the function, we use the UserModel Mongoose model. This is imported into the file and represents the example Mongoose model we created a moment ago.

The createANewUser() function takes three parameters:

  1. username
  2. password
  3. callback() function

The username and password represent the values entered by a user via your website. And the callback function is used to end the function once the task of saving the new user to the database is complete.

Inside the function, we create a new user database document using the new UserModel() method:

const newUserDbDocument = new UserModel({
  username: username,
  password: password
})

This creates a new document with the username and password function parameter values included.

Then, we use Mongoose's save() method to add the document to the database.

Before your password is saved, the pre method we added to the Mongoose model will hash your password.

After you create a new user in the database with that function, it will contain the user's username and hashed password.

Check A Hashed Password For Matches

Now that we've hashed a user's password and stored it in the database, we need a way to compare it to a string entered by a user (i.e. on a login page) and see if it matches the original password entered for that account.

The Compare Password Function

Luckily, Bcrypt has a built-in way to do this using their compare() method.

This method will take a password string and compare it to the hash stored in the database. If the two values match, it will return a value of true. If not, it will return false instead.

Here is an example of what the function would look like:

const bcrypt = require("bcryptjs")

const passwordEnteredByUser = "mypass123"
const hash = "$2a$10$FEBywZh8u9M0Cec/0mWep.1kXrwKeiWDba6tdKvDfEBjyePJnDT7K"

bcrypt.compare(passwordEnteredByUser, hash, function(error, isMatch) {
  if (error) {
    throw error
  } else if (!isMatch) {
    console.log("Password doesn't match!")
  } else {
    console.log("Password matches!")
  }
})

First, we create two variables called passwordEnteredByUser and hash.

passwordEnteredByUser represents the password a user would type in a login form and the hash variable is what would be stored in the database for a user.

Then, we use the bcrypt.compare() function to compare the passworedEnteredByUser and hash against each other. It has a callback function that returns the boolean result (true or false) representing whether or not the passwords match.

Last, we use console.log() to indicate whether or not the passwords matched.

If you provide a password that matches the original password used to create the hash, the following message should be logged:

Password matches!

If the passwords don't match, the following message should be outputted:

Password doesn't match!

Use Mongoose To Match The Passwords

Now that we know how to match passwords, let's go over how to implement it in the MongoDB database using Mongoose.

To do that, we need to add a comparePassword() method to the user Mongoose model:

UserSchema.methods.comparePassword = function(password, callback) {
  bcrypt.compare(password, this.password, function(error, isMatch) {
    if (error) {
      return callback(error)
    } else {
      callback(null, isMatch)
    }
  })
}

This method uses bcrypt to compare the password parameter (i.e. from a website login form) and the hashed password stored in the database. If the passwords match, a success message is returned. If not, a failure message is returned instead.

At this point, the entire Mongoose model should look like this:

const mongoose = require("mongoose")
const bcrypt = require("bcryptjs")

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

UserSchema.pre("save", function (next) {
  const user = this

  if (this.isModified("password") || this.isNew) {
    bcrypt.genSalt(10, function (saltError, salt) {
      if (saltError) {
        return next(saltError)
      } else {
        bcrypt.hash(user.password, salt, function(hashError, hash) {
          if (hashError) {
            return next(hashError)
          }

          user.password = hash
          next()
        })
      }
    })
  } else {
    return next()
  }
})

UserSchema.methods.comparePassword = function(password, callback) {
  bcrypt.compare(password, this.password, function(error, isMatch) {
    if (error) {
      return callback(error)
    } else {
      callback(null, isMatch)
    }
  })
}

module.exports = mongoose.model("User", UserSchema)

The Mongoose model is now all set!

Use The Compare Password Method

As an example, let's go over how the Mongoose model would be used in practice.

Let's create an imaginary scenario where you have a login form on your website that has both a username and password field.

When that user submits the field, you want to see if the user should be logged-in. This will only be allowed if the username is found in the database and matches the password entered via the login page.

The code for achieving that could look like the following:

const UserModel = require("./models/user.js")

module.exports = {
  loginUser: function(username, password, callback) {
    UserModel.findOne({username: username}).exec(function(error, user) {
      if (error) {
        callback({error: true})
      } else if (!user) {
        callback({error: true})
      } else {
        user.comparePassword(password, function(matchError, isMatch) {
          if (matchError) {
            callback({error: true})
          } else if (!isMatch) {
            callback({error: true})
          } else {
            callback({success: true})
          }
        })
      }
    })
  }
}

In this code, we declare a new function called loginUser() that takes three parameters:

  1. username
  2. password
  3. callback() function

The username and password values are what the user would have entered into the input fields on a login page of your website.

Inside the function, the first thing we do is get the user from the database with the given username. To do that, we use the UserModel.findOne() Mongoose method.

Once the user is found, we use the comparePassword() method we just built to see if the password provided to the function matches what's stored in the database.

If the passwords match, a successful result will be returned using the callback function. This means the login attempt was a success.

Otherwise, an unsuccessful result will be returned instead.

That was the last code we'll cover in this guide.

Happy coding!