Securely Saving Passwords in MongoDB Through Node.js

Securely Store Passwords in MongoDB Through Node.js with bcrypt Hashing for Maximum Security

It is important to protect sensitive data stored in web application databases from malicious actors. To do this effectively, it is essential that we use hashing instead of storing the data in plaintext. Hashing is a complex process which converts plaintext into a non-readable string of characters, which cannot be reversed or decrypted to the original text. By using hashing, it makes it much more difficult for malicious actors to gain access to the sensitive data and use it for malicious purposes, helping to ensure the security of that data.

This article covers the use of bcrypt to securely store user passwords in MongoDB through Node.js. It explains the project structure, setting up express web server, configuring MongoDB, creating an auth controller, and testing with postman. In conclusion, bcrypt is a secure algorithm for password hashing and is recommended for safe storage of sensitive data.

In this article, we'll use the npm library bcrypt to store user passwords in MongoDB. To demonstrate this, let's take an example of the sign-in process for a user. To keep things simple, there are just two fields: username and password.

In the end, we will construct the project structure in this way.

Screenshot 2021-05-04 at 5.49.55 PM.png

First, we will install the npm libraries.

npm install express mongodb bcryptjs body-parser cors --save

You can use mongoose instead of mongodb.

Creating an Express web server

Let's create a new file called server.js in the root folder, which will be our main file.

const express = require('express');
const bodyParser = require('body-parser');
const cors = require('cors');

const app = express();

const corsOptions = {
    origin: 'http://localhost:8081'
}
app.use(cors(corsOptions));

app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
app.use(function (req, res, next) {
    res.header(
        "Access-Control-Allow-Headers",
        "x-access-token, Origin, Content-Type, Accept"
    );
    next();
});
app.get('/', (req, res) => {
    res.json({ message: 'Welcome to Mongodb auth' });
});
require('./app/routes/auth.routes')(app);
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
    console.log(`Server running at ${PORT}`);
});

We are using body-parser to create the req.body object by parsing the request.

Setting up MongoDB

We will create a file called db.config.js within the config folder of the app, which will store all database-related configurations.

module.exports = {
    HOST:"localhost",
    PORT: 27017,
    DB: "bcryptdemo",
    USER: "users"
}

Let's now create a file called mongodb.js in the root folder for connecting.

const dbConfig = require('./config/db.config');
const MongoClient = require('mongodb').MongoClient;
let conn;

exports.getConnection = async () => {
    if (conn) {
        return conn;
    }

    try {
        conn = await MongoClient.connect(`mongodb://${dbConfig.HOST}:${dbConfig.PORT}/`, {
            useNewUrlParser: true,
            useUnifiedTopology: true
        });
        if (conn) console.log('Mongodb connected successfully!');
        return conn;
    }
    catch (err) {
        console.log('Error in mongodb connection', err);
        process.exit();
    }
}

Creating an Auth Controller

We have a signup and signin function to demonstrate hashing.

Signup

const bcrypt = require('bcryptjs');
const dbConfig = require('../config/db.config');
const conn = require('../mongodb');

exports.signup = async (req, res) => {
    const client = await conn.getConnection();
    const db = client.db(`${dbConfig.DB}`);
    bcrypt.hash(req.body.password, 12)
        .then(function (hashedPass) {
            const user = {
                username: req.body.username,
                password: hashedPass
            }
            db.collection(`${dbConfig.USER}`).insertOne(user);
            res.send({ message: 'Registered user successfully!' });
        }).catch((err) => {
            return res.status(500).send({ message: err });
        });
}

The bcrypt hash function takes two parameters - the password to be hashed, and the salt or saltrounds. In this case, it's set to 12, which means the bcrypt module will go through 2^12 iterations in order to hash the data.

The result of hash function looks like $[algorithm]$[cost]$[salt][hash]

$2a$12$A1iqBUXMMVEUAIimj9FJmeGLDwaLOz6g2i13WKVHEg3rteKLEYvLy
 |  |  |                     |
 |  |  |                     hash-value = GLDwaLOz6g2i13WKVHEg3rteKLEYvLy
 |  |  |
 |  |  salt = A1iqBUXMMVEUAIimj9FJme (base64 encoded to 22 characters)
 |  |
 |  cost-factor => 12 = 2^12 rounds
 |
 hash-algorithm identifier => 2a = BCrypt

Signin

To sign in, we need to find the hashed password from the database and use the compare function to compare the plaintext password with the hashed one.

exports.signin = async (req, res) => {
    const client = await conn.getConnection();
    const db = client.db(`${dbConfig.DB}`);
    db.collection(`${dbConfig.USER}`).find({ username: req.body.username }).toArray((err, user) => {
        if (err) return res.status(500).send({ message: err });
        if (!user[0]) return res.status(404).send({ message: 'User not found' });
        bcrypt.compare(req.body.password, user[0].password).then((isPassValid)=>{
            if (!isPassValid) return res.status(401).send({ message: 'Invalid Password!' });
            res.send({
                id: user[0]._id,
                username: user[0].username,
                message: 'Signed in successfully!'
            });
        }).catch((err) => {
            return res.status(500).send({ message: err });
        });
    });
}

compare function returns a boolean.

We can add middleware to prevent duplicate entries when signing up.

app.post("/auth/signup", [verifySignUp.checkDuplicateUsername], AuthController.signup);

In verifySignUp.js, let's add the below code snippet:

checkDuplicateUsername = async (req, res, next) => {
    const client = await conn.getConnection();
    const db = client.db(`${dbConfig.DB}`);
    db.collection(`${dbConfig.USER}`).find({ username: req.body.username }).toArray((err, user) => {
        if (err) return res.status(500).send({ message: err });
        if (user[0]) {
            return res.status(400).send({ message: `Username ${user[0].username} already exists` });
        }
        next();
    });
};

Testing

We will use Postman to test the signup and sign-in process and check MongoDB to see if the password was saved.

Screenshot 2021-05-07 at 5.34.21 PM.png

Screenshot 2021-05-07 at 5.36.04 PM.png

For some negative test cases:

Let's try adding the same user!

Screenshot 2021-05-07 at 5.43.33 PM.png

Entering an incorrect password.

Screenshot 2021-05-07 at 5.42.03 PM.png

We can view the password in its hashed form in MongoDB.

Screenshot 2021-05-04 at 6.28.13 PM.png

In conclusion, bcrypt is a secure algorithm for password hashing and is recommended for safe storage of sensitive data. It is more secure than other methods of password hashing due to its use of a cost factor, which makes it more difficult for attackers to brute force the password. Additionally, it is an easy to use library that is available in Node.js and can be easily integrated into web applications.

Did you find this article valuable?

Support Ipseeta Priyadarshini by becoming a sponsor. Any amount is appreciated!