Storing user password with hashing


Introduction

In this lab, we will learn how to use Nodejs to handle user login and storing password in a database system. As usual in our labs, we will use MongoDB as our database system. We will revisit the backend route handler for handling password securely.

Rather than storing the password in plain text, we will use a hashing function to hash the password before storing it in the database. We will also use a hashing function to hash the password entered by the user and compare it with the hashed password stored in the database. If the two hashed passwords match, we will consider the user as authenticated.

NVM installation

In case you got no Node.js in your machine you could install it with NVM. Please follow the instructions in the following link:

For macOS or Linux:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash

After that, you could install Node.js with the following command:

nvm install node

To make it work in any terminal, you could add the following line to your ~/.bash_profile in macOS:

export NVM_DIR="$([ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm")"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm

And I have prepare a simplied version for you.

curl https://gist.githubusercontent.com/hkbu-kennycheng/fa9cb4a52651e24626d26a90603af9de/raw/5a902dd66ff334733f7685de5944d5e4b3895f0f/.bash_profile >> ~/.bash_profile

Create an new express project for backend

Let’s start with creating a new express project for backend. We will use the same project structure as we did in COMP3047. We will create a new folder called backend and create a new express project inside it. We will use the express-generator to create an new express project as below:

npx express-generator --no-view --git backend

Note that --no-view option will create a project without any view engine. And we will use Vue@3 for frontend. --git option will create a git repository for the project.

After that we will install all the dependencies for the project:

cd backend
npm install

Now we can start the project and test it:

npm start

Revisiting Express project structure and configuration

The project structure should look like this:

backend
├── app.js
├── bin
│   └── www
├── package-lock.json
├── package.json
├── public
│   ├── images
│   │   └── favicon.ico
│   └── javascripts
└── routes
    ├── index.js
    └── users.js

Application entry and app.js

The main entry point of an Express application is bin/www. It’s is a node script that starts the server and listens on port 3000 for connections. The first line of bin/www requires app.js in the project root. That’s the main progrom of an Express application. Let’s open up app.js and take a look at the code:

var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');

var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');

var app = express();

app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', indexRouter);
app.use('/users', usersRouter);

module.exports = app;

In the codes above, you could see that we have imported index and users from routes folder. We will take a look at them later. The app.use function is used to register middleware. We will talk about middleware later in this lab. For now, you could just think of it as a function that will be called before the request is handled by the route handler.

Routes

The routes register in app.js are defined in routes/index.js and routes/users.js. They are registered as URL prefix / and /users respectively. Meaning that route handler functions defined in routes/index.js would have a URL path starting with /. For route handler functions in routes/users.js got an URL path prefix /users.

For today’s Lab, we will mainly work on routes/users.js to add user creation route handler in it. Let’s take a look at routes/users.js:

var express = require('express');
var router = express.Router();

/* GET users listing. */
router.get('/', function(req, res, next) {
  res.send('respond with a resource');
});

module.exports = router;

Connecting to MongoDB in Express

Please install mongodb package in your project:

npm install mongodb --save

Then we can connect to MongoDB in routes/app.js with the following code. Please note that, you will need to replace connectiong string with yours.

const { MongoClient } = require("mongodb");
// Replace the uri string with your connection string.
const uri = "mongodb+srv://<username>:<password>@<endpoint>.mongodb.net/?retryWrites=true&w=majority";
const client = new MongoClient(uri);
const db = client.db('bookingDB');

It will create an new database called bookingsDB if it doesn’t exist. We will use this database to store user information.

User creation route handler with password hashing

Now we can start working on the user creation route handler. We will use bcrypt package to hash the password before storing it in the database. Please install bcrypt package in your project:

npm install bcrypt --save

Then let’s create a new route handler for user creation. We will use POST method to create a new user. The URL path for this route handler will be /users/. The route handler will take the user information from the request body and store it in the database. Before inserting into database, the user password would go through a hashing function with generated salt to make sure it’s not stored in plain text.

const bcrypt = require('bcrypt');
const saltRounds = 10;

router.post('/', async function(req, res, next) {
  let user = req.body;
  const salt = bcrypt.genSaltSync(saltRounds);
  const hash = bcrypt.hashSync(user.password, salt);

  try {
    const result = await db.collection('users').insertOne({
      username: user.username,
      password: hash,
      role: user.role
    });
    return res.status(201).json(result);
  } catch(e) {
    return res.status(500).json(e);
  }
});

saltRounds in brcypt is the number of rounds to process the data for. The more rounds you set, the more secure the hash will be, but the more time it will take to hash the data. The default value is 10. You can read more about it in the documentation.

Here is a flow chart in mermaid.js of how the password hashing works:

graph LR
A[User] -->|JSON| B[POST /users]
B -->|saltRounds| C[bcrypt.genSaltSync]
C -->|salt+password| D[bcrypt.hashSync]
D -->|hash string| E[insertOne]

After that, the route handler will return a JSON object with status code 201 if success. The HTTP 201 Created success status response code indicates that the request has succeeded and has led to the creation of a resource. Otherwise, a 500 status code would return.

Testing RESTful API with Postman

Now we can test the user creation route handler with Postman. Please make sure you have installed Postman. If not, please install it from here.

First, we need to start the Express server:

npm start

After that, let’s open up Postman. Click on the + button to create a new request. Then select POST method and enter the URL path /users. Then click on the Body tab and select raw and JSON from the dropdown menu. Then enter the following JSON object in the text area:

{
  "username": "admin",
  "password": "123456",
  "role": "admin"
}

Then click on the Send button. You should see a response with status code 201 and a JSON object with the user information.

Viewing data in MongoDB with VS Code extension

Now let’s take a look at the data we just created in the database. We can use VS Code extension to view the data. Please install MongoDB for VS Code extension from here.

After that, there is a new side-tab appeared in VS Code. Click on the MongoDB tab and click on the Connect button. Then enter the connection string in the input box. You can find the connection string in the Connect tab of your MongoDB Atlas cluster. Then click on the Connect button. You should see a new database called bookingDB in the MongoDB tab. Click on the bookingDB database and you will see a new collection called users. Click on the users collection and you will see the user information we just created.

Create a login route handler

After user creation, let’s create a login route handler. We will use POST method to login a user. The URL path for this route handler will be /users/login. The route handler will take the user information from the request body and compare it with the user information in the database. If the user information is correct, the route handler will return a JSON object with status code 200. Otherwise, a 401 status code would return.

router.post('/login', async function(req, res, next) {
  let user = req.body;
  try {
    const result = await db.collection('users').findOne({
      username: user.username,
    });
    if (result) {
      const match = bcrypt.compareSync(user.password, result.password);
      if (match) {
        delete result.password;
        // you will need to combie JWT token with user information here.
        return res.status(200).json(result);
      } else {
        return res.status(401).json({message: 'Incorrect password'});
      }
    } else {
      return res.status(401).json({message: 'User not found'});
    }
  } catch(e) {
    return res.status(500).json(e);
  }
});

After that, we could try to login with the user we just created in Postman. If the user information is correct, the route handler will return a JSON object with status code 200. Otherwise, a 401 status code would return.

Let’s try out the newly create login API by Postman. First, we need to re-start the Express server:

^C # press Ctrl+C to stop the server
npm start

Open up Postname and click on the + button to create a new request. Then select POST method and enter the URL path /users/login. Then click on the Body tab and select raw and JSON from the dropdown menu. Then enter the following JSON object in the text area:

{
  "username": "admin",
  "password": "123456"
}

Then click on the Send button. You should see a response with status code 201 and a JSON object with the user information.