User authentication is the process of verifying the identity of a user attempting to gain access to your application. It involves authorizing and transferring credentials to confirm a user's authenticity.

You can implement a simple user authentication model in Node.js using Express, Bcrypt, and MongoDB, in just a few steps.

Step 1: Setting Up Development Environment

First, create a project folder and cd into it by running:

        mkdir user-authentication
cd user-authentication

Next, initialize npm in your project directory by running:

        npm init -y

The -y flag initializes npm and creates your package.json file with all its defaults.

This user authentication model requires a few dependencies.

They include:

  • Express: Express is a Node.js framework that provides a robust set of features for web and mobile applications. It makes it easier to build backend applications with Node.js.
  • Bcrypt: bcrypt is an npm package that implements the bcrypt password hashing function. It allows you to create hashes from plain password strings.
  • Mongoose: Mongoose is a MongoDB object data modeling library. It simplifies the interactions between your app and a MongoDB database.
  • dotenv: dotenv is a zero-dependency package that loads environment variables from a .env file into process.env.
  • Validator: validator is a package that contains various string validation functions.
  • Body-parser: The body-parser package parses request bodies in a middleware before your handlers.

Install the packages by running:

        npm install express bcrypt mongoose dotenv validator body-parser

Next, create an app.js file in your project’s root directory and add the code block below to create a basic Express server:

        // app.js
const express = require('express');
const app = express();
const bodyParser = require("body-parser");
 
const port = 3000;
 
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
 
app.listen(port, ()=>{
    console.log(`App is listening on port ${port}`);
});

This code creates an express application instance by calling the express function. It then uses the body-parser middleware to parse incoming request bodies. Then it starts listening for traffic on port 3000 by calling the express instance’s listen method and passing the port variable as an argument.

Step 2: Connecting Your Application to a Database

In your project’s root directory, create a .env file and store your MongoDB credentials in it. This avoids exposing your database credentials in code which can give malicious users access to your database.

Next, navigate to your app.js file and import mongoose:

        const mongoose = require("mongoose");

Then, call import dotenv and call the config method on it:

        require("dotenv").config();

Calling the config method on dotenv loads environmental variables into the process.env.

Finally, call the connect method on mongoose and pass your MongoDB URI as an argument:

        mongoose.connect(process.env.MONGODB_URI).then(() => {
    console.log('Connected to Database Successfully')
})

Step 3: Creating User Model

In your project’s root directory, create a “models” folder; this is where you will store your mongoose model:

        mkdir models

Next, create a “userModel” file and add the following imports:

        const mongoose = require('mongoose')
const { isEmail } = require('validator')

isEmail is a validation function that returns true if a given string is an email. You will need it to apply mongoose validation to your user model.

Next, add the following code to your userModel file:

        // models/userModel
const userSchema = mongoose.Schema({
    email: {
        type: String,
        required: [true, 'Email is required'],
        validate: {
            validator: isEmail,
            message: props => `${props.value} is not a valid email`
        }
    },
 
    password: {
        type: String,
        required: [true, 'Password is required'],
        validate: {
            validator: function (value) {
                return value.length >= 6
            },
            message: () => 'Password must be at least six characters long'
        }
    }
})
 
module.exports = mongoose.model('User', userSchema)

The code above creates a userSchema variable that stores the value of the mongoose.Schema method. The mongoose.Schema method maps properties to a MongoDB collection and defines the shape of the documents within it. The mongoose schema has two properties—an email and a password—which will be your authentication requirements.

The email property is a string type and has required set to true. The accompanying error message, “Email is required,” will display if a request body does not contain an email property. Finally, using mongoose custom validation, the validator property referenes the isEmail function. That function returns true or false based on the validity of the string as an email. Then the message property takes the email value (props) and constructs a meaningful error message.

The password property is a required string type with an error message that reads “Password is required”. The validator function is an anoymous one that returns true if the password is at least six characters long.

The final line creates and exports a mongoose model by calling the model method on mongoose. Pass the model name (User) as the first argument and a schema (userSchema) as the second argument.

Step 4: Implementing Sign-in and Sign-up Routes

In your project’s root directory, create a routes folder:

        mkdir routes

In your routes folder, create a userRoutes.js file and add the following imports:

        // routes/userRoutes.js
const express = require("express");
const User = require("../models/userModel");
const bcrypt = require("bcrypt");

Create an Express Router instance by calling the Router method on express:

        const router = express.Router();

Next, create your sign-up route by adding the code block below to your userRoute.js file:

        router.post("/sign-up", async (req, res) => {
  try {
    // Extract email and password from the req.body object
    const { email, password } = req.body;
    
    // Check if the email is already in use
    let userExists = await User.findOne({ email });
 
    if (userExists) {
      res.status(401).json({ message: "Email is already in use." });
      return;
    }
 
    // Define salt rounds
    const saltRounds = 10;
 
    // Hash password
    bcrypt.hash(password, saltRounds, (err, hash) => {
      if (err) throw new Error("Internal Server Error");
 
      // Create a new user
      let user = new User({
        email,
        password: hash,
      });
 
      // Save user to database
      user.save().then(() => {
        res.json({ message: "User created successfully", user });
      });
    });
  } catch (err) {
    return res.status(401).send(err.message);
  }
});

In the code block above, first, you de-structured the email and password from the req.body object. Then, check if a user is already using the email because it should be unique for each user. If the email has already been used, you return and stop code execution with a 401 status code.

Storing plain passwords in a database is a huge security threat as malicious hackers might gain access to the database. You should hash passwords before sotring them in your database, so even if a hacker discovers them, there shouldn't be a risk to users. Hashing is the process of converting a given “key” into another value. Hashing is a one-way function, which means that you cannot retrieve the original value from the hased one, unlike encryption.

Using bcrypt, you hashed your user password by calling the hash method on bcrypt. The hash method takes three parameters: the string to be hashed, salt rounds, and a callback function. You pass the user password, the saltRounds variable you created earlier, and a callback.

Salt rounds refer to the time needed to calculate a single bcrypt hash. The higher the salt rounds, the more the hashing rounds.

If the hash method throws an error, you throw an “internal server error.” Else, you set the password property to the successful hash and save it to your database by calling the save method on the User instance.

Next, create your sign-in route by adding the code block below to your userRoute.js file:

        router.post("/sign-in", async (req, res) => {
  try {
    // Extract email and password from the req.body object
    const { email, password } = req.body;
 
    // Check if user exists in database
    let user = await User.findOne({ email });
 
    if (!user) {
      return res.status(401).json({ message: "Invalid Credentials" });
    }
 
    // Compare passwords
    bcrypt.compare(password, user.password, (err, result) => {
      if (result) {
        return res.status(200).json({ message: "User Logged in Successfully" });
      }
      
      console.log(err);
      return res.status(401).json({ message: "Invalid Credentials" });
    });
  } catch (error) {
    res.status(401).send(err.message);
  }
});
 
module.exports = router;

In the code block above, first, you de-structure the email and password from the req.body object. Then, you check if a user exists in your database. If the user doesn't exist in your database, you return with a 401 status code.

Next, using bcrypt’s compare method, pass in the password the user provided and the hashed password you retrieved from your database. Compare the two to confirm if they match. If the passwords match, you return a 200 status code and a success message. Else you return a 401 status code and an error message.

Finally, import router into your app.js file and use it as an application-level middleware.

This completes your user authentication model; now, users can securely sign-up and sign in to your application.

The Importance of User Authentication

User authentication ensures that only legitimate users can gain access to your application. If your data is in any way personal or private, you should take steps to preventing unauthenticated users from gaining access.