Encrypting passwords using bcrypt in Node.js

Username and password are still the primary way of authenticating users for web applications. Securely storing user credentials is essential to prevent them from being compromised. In this post, I'll show how to use bcrypt to encrypt passwords and how to store them in the database.

I’ll attempt to create a series of posts about authentication using Node.js. I’ll start from the traditional approach and then gradually move on to federation and serverless authentication using AWS.

The size of the posts will come in smaller, easy-to-read chunks, and in the end, they can be put together to form a larger working unit.

The first post in this series will be about using bcrypt as a way to encrypt passwords.

1. About authentication

Authentication is the process of checking if the user is who they say they are. It’s usually checked through the combination of username and password, and this method gets more and more often combined with additional steps like two-(or multi-)factor authentication (2FA) to ensure that the user is legit.

The basic process is that the user first registers (or signs up) by creating a username and password. This password is then stored in a secure place (database).

When the user wants to log in, their username and password will be compared to those stored in the database.

If there is a match, the user can be redirected to the private content (or can be prompted to enter their code in case of 2FA).

If the username or password the user entered doesn’t match those stored in the database, the entry to the private content is denied.

2. Encrypting passwords

The number one rule is that one should never store raw passwords in the database.

2.1. Don’t store raw passwords

This is probably one of the most serious sins against security a developer can commit. Databases can be breached, and unencrypted user credentials can be compromised, so it’s important to make the thieves’ life harder.

2.2. Store encrypted passwords

Instead of storing raw passwords, we store the encrypted version of the password. This usually means hashing, which is a one-way method of obfuscating passwords.

This means that if something gets hashed, it cannot be unhashed, which makes hashing suitable for making passwords unrecognizable. The same input will always result in the same output.

It’s very hard to find out the original password once it’s hashed (although it’s not impossible).

2.3. Use bcrypt

We could use the crypto module for hashing passwords but there is a better alternative, and this is bcrypt.

Bcrypt hashes and salts passwords. Salt is a random data added to the hashing process, and this makes the password hash unique. This way, passwords hashed with bcrypt are considered to be secure, because salted and hashed passwords are extremely hard to guess (let’s ignore quantum supremacy for now).

3. The code

Let’s talk about the steps and the stack I’ll use for encrypting and storing user credentials.

I’ll use an Express server with routes and middleware. The server can later be deployed to EC2 instance or an ECS container, which is beyond the scope of this post.

User data will be stored in MongoDB for now, and I’ll use Docker to create the database. Why install the complete MongoDB if we can use Docker?

I’ll use Node.js version 12, because it’s the long term support version as at today.

3.1. Set up

Let’s create a project folder, and for the sake of simplicity, I won’t have any other folders. Everything will be placed in the root folder.

Set up the folder with npm init -y, this command will create the package.json file.

I really like docker-compose and we can use it to provision the MongoDB database. The docker-compose.yml file can look like this:

version: "3"

services:
  db:
    image: mongo:latest
    volumes:
      - mongo-data:/data/db
    ports:
      - 27017:27017

volumes:
  mongo-data:

As with every .yml file, indentation is important here. Other than that there are no miracles in the file. We’ll run the container off the latest mongo image, which is managed by the Mongo team. I’m using a named volume here (mongo-data), this will store the data. I’m also opening up port 27017 (the first port value is the host, i.e. your computer; the second value is for the Docker container).

Let’s run docker-compose up in the terminal from the root folder, and the database should soon wait for connections on port 27017.

3.2. Packages to install

The following packages will be needed for the code coming below to work:

npm install express mongodb bcrypt body-parser

The list will be expanded in the following posts, but for now they will suffice to demonstrate how bcrypt works.

3.3. Let’s bcrypt it

If the packages are installed, we can require them at the top of the file called server.js:

const express = require('express')
const { MongoClient } = require('mongodb')
const bodyParser = require('body-parser')
const bcrypt = require('bcrypt')

const app = express()

const PORT = 3000
const MONGO_URL = 'mongodb://localhost:27017'

app.use(bodyParser.json())

I’m using the native MongoDB driver (again, nothing fancy here), and we’ll need the MongoClient class from the module.

The app.use(bodyParser.json()) middleware ensures that the data in the req.body will be converted to JSON format before the data arrives at the route handler.

Connect to the database. We can now set up the database connection. I’m creating a main function creatively called main, and the logic will be placed in the body of that function:

async function main() {
  let client
  try {
    client = await MongoClient.connect(MONGO_URL)
    console.log('Connected to database')
  } catch (e) {
    throw new Error('Error while connecting to database')
  }

  const db = client.db('authentication-with-node')

  // logic will come here

  // last lines of the function body
  app.listen(PORT, () => {
    console.log(`Server is listening on ${ PORT }`)
  })
}

main().catch((e) => {
  console.log(e)
})

The connect static method on the MongoClient class returns a Promise, and because main is an async function, we can await for the value the Promise is resolved with.

The name of the database is authentication-with-node.

Public endpoint. The server will expose a public endpoint:

app.get('/', (req, res) => {
  res.send('Public endpoint')
})

This endpoint won’t do anything fancy, it will simply return the Public endpoint string.

We can quickly check if it’s working by starting the server:

node server.js

Then, a simple curl command should do the job:

curl localhost:3000

The /register endpoint

Users need to register themselves before trying to log in to the application.

The /register endpoint seems to be a sensible name for this task:

// public endpoint here
// ...

// register endpoint
app.post('/register', async (req, res, next) => {
  const { username, password } = req.body

  if (!username || !password) {
    return next(new Error('No username or password'))
  }

  if (password.length < 10) {
    return next(new Error('Password must consist of at least 10 characters'))
  }

  try {
    const hashedPassword = await bcrypt.hash(password, 10)
    await db.collection('Users').insertOne({
      username,
      password: hashedPassword,
    })
    res.send('User data have been saved')
  } catch (e) {
    return next(new Error('Unknown error'))
  }
})

The /register endpoint will accept a POST request and the username and password will be sent to the server in the request body (see bodyParser above).

After some validation we can hash the password. Bcrypt’s hash method does both hashing and salting. The first argument is the password, and the second can be a number or a secret string, in this case, it’s 10. This number refers to the number of rounds bcrypt will go through to hash the data with the salt being generated and used (and it’s more secure as well). The greater this number is, the longer it takes for bcrypt to create the hash. It’s growing exponentially, so you’ll need to make a trade off here. It’s not a great user experience to wait hours for the server to hash the password (although some funny cat or swimming fish gif might want some users to stay a bit longer).

If you provide a string instead of a number, then it will be used as a salt.

After bcrypt finished doing its job, we save both the username and the encrypted password (hashedPassword) to the Users collection of the database.

The /login endpoint. The user has their credentials saved in the database now, so they can log in.

As it was discussed above, there’s no such thing as unencrypting passwords, because hashing is a one way street. Instead, bcrypt will do the same procedure with the password the user enters (i.e. hashing and salting), and then it will compare the entered password with the one stored in the database.

They can be safely compared, because the same input will result in the same output.

If they match, we can let the user to access the private content. If they don’t match, we can return an error message.

Here’s a possible route handler for the /login endpoint:

// login endpoint
  app.post('/login', async (req, res, next) => {
    const { username, password } = req.body

    if (!(username && password)) {
      return next(new Error('Missing username or password'))
    }

    const { username: user, password: hashedPassword } = await db
      .collection('Users')
      .findOne({ username })

    if (username !== user) {
      return next(new Error('Incorrect username/password'))
    }

    const isMatchingPassword = await bcrypt.compare(password, hashedPassword)

    if (!isMatchingPassword) {
      return next(new Error("Incorrect username/password"))
    }

    res.send('Authentication successful')
    // redirect user to the private page
  })

The code contains lots of validations. The core of the logic occurs towards the end of the snippet.

The compare method accepts both passwords: the one the user has just entered (password), and the one stored in the database (hashedPassword). The name of the method pretty much tells everything about its function: It compares the two passwords, and returns a Promise which will resolve to true (for matching passwords) or false.

If everything goes well, the Authentication successful message will be returned.

Error handler. In the code, I’m wrapping the errors in the next function, which is a built-in Express function. It will forward the error messages to the error handler, which can be placed at the end of the code, just before the listen block:

// error handling
app.use((err, req, res, next) => {
  res.status(err.httpStatusCode || 400).json({ message: err.message })
})

// app.listen(...)

3.4. Try it

Save the file and start the server:

node server.js

First, let’s register the user using curl:

curl -X POST -H 'Content-Type: application/json' \\
-d '{"username": "james.bond", "password": "mysupersecretpassword"}' localhost:3000/register

This command should return the User data have been saved message and at the same time, the credentials should be saved to the database.

Now James can log in:

curl -X POST -H 'Content-Type: application/json' \\
-d '{"username": "james.bond", "password": "mysupersecretpassword"}' localhost:3000/login

If everything goes well (and it should for James Bond), the authentication will be successful, and Mr Bond can access the private content.

4. Summary

Authentication is a crucial part of any applications. It usually includes the registration of a username and password, which are stored in a database.

It’s a basic security principle that passwords are never saved in their raw form to the database. They should only be stored in an encrypted format.

Bcrypt is a secure way to hash and salt passwords. Salting adds an extra security layer to the hashing process by incorporating a random secret (salt).

Thanks for reading and see you next time.