Storing user password with hashing
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.