Securing passwords with Bcrypt in Nodejs and MongoDB

Securing passwords with Bcrypt in Nodejs and MongoDB

·

4 min read

In this article, I will explain how to secure user passwords with a bcrypt hashing algorithm.

Hashing and Encryption

The hashing algorithm is a one-way communication, which means if a password is encoded to a hashed password, it cannot be decoded to its original form. I want to explain it further by comparing it to an encryption algorithm, a two-way algorithm where the encoded password can be decoded to its original value if a password is encoded. for example, if you encrypt "qwerty" the result will be "TIEFJbwI" but you can easily decrypt it back to "qwerty" with the help of tools like this.on the other handing if you hash "qwerty", the result will look like similar to this "65e84be33532fb784c48129675f9eff3a682b27168c0ea744b2cf58ee02337c5" and it cannot be de-hashed back to "qwerty".

There are lots of hashing algorithms like MD5, SHA-256, SHA-512, SHA-1, and Bycrpt. You may have a question in mind that why we are choosing bcrypt instead of any other algorithm?.

We are choosing bycrpt simply because it is the safest hashing algorithm and below is a logarithmic bar chart that visualizes the time it takes to brute force the test password hashed by a specific hashing algorithm. ( source ) hashing_algorithm.PNG

Hashing in NodeJs

The first step is to install bcrypt.js in the NodeJs backend directory of your application.

npm i bcryptjs

Whenever we get a post request from the user for registration that also contains his password, we can use the bycrpt to hash the password.

The hashing can occur either in the function handling the registration route in Nodejs or in a middleware that runs whenever we try to save a new user but generally, hashing in the middleware is recommended.

Writing the middleware

Below is the function handling register route

router.post('/register', async (req, res) => {
  try {
    const { email, password } = req.body

    if (!email || !password) throw createError.BadRequest()

    const doesExist = await User.findOne({ email: result.email })
    if (doesExist)
      throw createError.Conflict(`${result.email} is already been registered`)

    const user = new User(result)
    const savedUser = await user.save()

  } catch (error) {
    return res.status(400).json({error});
  }
})

So a middleware that is inside the mongoose will be fired whenever the user.save method will be called in the above function.

In the userModel.js file, we can define the middleware where the user Schema is defined, and the callback function in the middleware will be executed before we save the new user.

const mongoose = require('mongoose')
const Schema = mongoose.Schema
const bcrypt = require('bcrypt')

const UserSchema = new Schema({
  email: {
    type: String,
    required: true,
    lowercase: true,
    unique: true,
  },
  password: {
    type: String,
    required: true,
  },
})

// execute the callback function whenever a new document is saved
UserSchema.pre('save', async function (next) {
  try {
    if (this.isNew) {
      const salt = await bcrypt.genSalt(10)

      const hashedPassword = await bcrypt.hash(this.password, salt)

      this.password = hashedPassword
    }
    next()
  } catch (error) {
    next(error)
  }
})

We cannot use arrow functions as a callback function because we want to use 'this' keyword in the callback function, and if we use the arrow function, the value of 'this' is not bound to that arrow function; instead, it is inherited by the parent scope and in the above case window in the parent scope. So you should avoid using the arrow function in the event listeners. You can read more about this here.

In the try block first, check if the document is new by using a helper of mongoose .isNew, therefore, this.isNew is true if the document is new else false, and we only want to hash the password if its a new document, else it will again hash the password if you save the document again by making some changes in other fields in case your document contains other fields.

If the condition is satisfied, the first step is to generate a salt using the bcrypt package. salt is nothing but a random string you attach to the password to make the hashed password more secure. genSalt method is a method that takes an argument round to generate a salt. Here round means the cost factor. The cost factor controls how much time is needed to calculate a single BCrypt hash. The higher is the value of the round, the stronger is your password, but the time to save the user will also increase; therefore, we generally use 8 or 10 rounds.

The next step is straightforward. There is a hash function that takes the hashed password and the salt and returns the hashed password, which replaces the current regular password of the user.

So, finally, after all the hashing since it is a middleware, the next() method is called to save the user in our MongoDB collection.

Thanks for reading all the way through. I hope you learned something new from this article. Feel free to comment on your thoughts.