Node.js Authentication and Authorization with JWT: Building a Secure Web Application
Tags: authentication, node, jwt, jsonwebtoken
Published: 2023-10-18 13:13:50

## Table of Contents

1. [Introduction](#introduction)
2. [Prerequisites](#prerequisites)
3. [Set up file structure](#set-up-file-structure)
4. [Install the necessary dependencies](#install-the-necessary-dependencies)
5. [Set up the database](#set-up-the-database)
6. [Set up express server](#set-up-express-server)
7. [Create the user model](#create-the-user-model)
   - 7.1 [Define a mongoose schema and model](#define-a-mongoose-schema-and-model)
   - 7.2 [Middleware for password hashing](#middleware-for-password-hashing)
   - 7.3 [Custom method for user model](#custom-method-for-user-model)
8. [Create Joi schema for data validation](#create-joi-schema-for-data-validation)
9. [Create auth middleware](#create-auth-middleware)
10. [Create the controller](#create-the-controller)
11. [Create the routes](#create-the-routes)
12. [Serve the express application](#serve-the-express-application)
13. [Test with Postman](#test-with-postman)
   - 13.1 [Create request](#create-request)
14. [Conclusion](#conclusion)

## **Introduction**

**What is JSON Web Token (JWT)**

JWT(JSON Web Token) is a token format. It is self-contained and signed. It offers a practical method of data transfer. While JWT is not secure, using it can ensure message authenticity as long as you can verify the payload's integrity and confirm the signature. Stateless authentication using JWT is often used in simple cases involving non-complex systems.

In this article, we’ll be implementing authentication with JWT in a NodeJS web application.

**Here’s the example of JWT**

```
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6ImUzMTgyZmEzLTYwOGEtNDUwMC04MDMzLTU2YWE4ODIyZDNhMiIsImlhdCI6MTY5NzQ5OTMzMywiZXhwIjoxNjk4MTA0MTMzfQ.Hl9HJlpBSxsIbHUwIWen90HwyxbIBlwIABaZiXUuv4s

```

In this tutorial, we will learn how to build authenticated and authorized applications in Nodejs. Check out the final repository on [GitHub](https://github.com/Taiwo17/Auth_User)

## **Prerequisites**

* Basic knowledge of Javascript and ES6 syntax
* Installation of Nodejs on your system
* Basic knowledge of MongoDB
* Installation of Postman on your system

## **Set up file structure**

Create a folder anywhere on your computer and name it 

Open it in any text editor of your choice (Am using VS Code to open mine), open the terminal and run:


```
npm init -y 
```


Create files and directories using this command below, assuming you have installed Git on your system:


```
mkdir config models routes middleware controllers validators
```



```
touch config/database.js models/user.model.js routes/user.route.js middleware/auth.js controllers/user.controller.js validators/user.validator.js
```


Also, create the server file, the environment variable and the gitignore file in your root directory:


```
touch server.js .env .gitignore
```

Go ahead to setup a file structure like this below: 

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i4122au5gx2cdopnby2o.png)



## **Install the necessary dependencies**

Our project requires multiple npm packages. Below, you'll find a list of these packages along with brief explanations of how each one contributes to our objectives.



* **Express.js:** A node.js framework called Express.js makes web application development simple.
* **MongoDB:** The official MongoDB driver for Node.js is Mongodb.

* **Mongoose:** mongoose is an object modeling tool designed to function in an asynchronous mongoose setting. To create database schemas and communicate with the database, we will use Mongoose.

* **Bcryptjs:** Hash user passwords before putting them in the database.

* **JSON Web Token (JWT):** "We will use JWT for permission and authentication."

* **Joi:** Joi is JavaScript's most potent schema description language and data validator. With the aid of this package, you may create secure routes only accessible by logged-in users.

* **Nodemon:** Nodemon will restart the express server whenever we modify our code.

* **UUID:** The UUID package offers tools for creating standard UUIDs that are cryptographically safe. 

* **Dotenv:** This zero-dependency module loads environment variables into a process by reading them from the .env file.

* **Cookie-parser:** A middleware that parses cookies associated with the client request object.

    ```
npm install express mongoose bcryptjs jsonwebtoken joi uuid dotenv cookie-parser
```



**Add the development dependencies with this command:**


```
npm install nodemon -D 
```


## **Set up the database**

In this tutorial, we will use the MongoDB atlas for our database. Head over to MongoDB Atlas and click on Start free to create an account. After creating an account, MongoDB requires extra configuration. For more details, see the official [documentation ](https://www.mongodb.com/docs/guides/atlas/cluster/)


```
// config/database.js
const mongoose = require('mongoose')

exports.connectDB = async () => {
  try {
    await mongoose.connect(process.env.DB_ONLINE_URI)
    console.log(`Connected to database`)
  } catch (error) {
    console.log(error.message)
  }
}
```


This code exports a function connectDB that connects to a MongoDB database using Mongoose. It uses an environment variable to retrieve the database URI and logs a success message if the connection is established, or logs any encountered error messages.

## **Set up express server**


```
const express = require('express')
const { connectDB } = require('./config/database')
const dotenv = require('dotenv').config()
const cookieParser = require('cookie-parser')
const PORT = process.env.PORT

// Instantiating the mongodb database
connectDB()

// Instantiating the express application

const server = express()

server.get('/', (req, res) => {
  return res.json({
    message: 'This the Home page',
  })
})

// Express inbuilt middleware
server.use(express.json()) // Used in passing application/json data
server.use(express.urlencoded({ extended: true})) // Used in passing form
server.use(cookieParser()) // Used in setting the cookies parser

// Creating the server
server.listen(PORT, () => console.log('Server is running on port ' + PORT))
```




* The code actively sets up an Express web server.
* Define a simple route for the root URL
* Configures Express to use various built-in middleware. 
* Connects to a MongoDB database using the connectDB function. 
* The server actively listens on the port specified by the PORT environment variable.

## **Create the user model**

We’ll define our schema for the user details when signing up for the first time. We will be using mongoose to create UserSchema.

Add the following snippet to `user.model.js` inside the `model` folder.


```
const mongoose = require('mongoose')
const { v4 } = require('uuid')
const bcrypt = require('bcryptjs')
const jwt = require('jsonwebtoken')

const { Schema, model } = mongoose

const userSchema = new Schema(
  {
    _id: { type: String, default: v4 },
    name: {
      type: String,
      required: [true, 'Please provide your name'],
    },
    email: {
      type: String,
      required: [true, 'Please provide a valid email'],
    },
    password: {
      type: String,
      required: [true, 'Please provide a password'],
      select: false,
    },
    role: {
      type: String,
      enum: {
        values: ['user', 'author', 'contributor'],
        message: 'Please select your role',
      },
      default: 'user',
    },
  },
  {
    timestamps: true,
  }
)

userSchema.pre('save', function (next) {
  if (this.isModified('password')) {
    this.password = bcrypt.hashSync(this.password, 12)
  }
  next()
})

userSchema.methods.comparePassword = async function (enterPassword) {
  return bcrypt.compareSync(enterPassword, this.password)
}

userSchema.methods.jwtToken = function () {
  const user = this
  return jwt.sign({ id: user._id }, 'random string', {
    expiresIn: '1h',
  })
}

const User = model('User', userSchema)

module.exports = User

```


### **Define a mongoose schema and model:**

The code defines a Mongoose schema for a user object. The schema specifies the structure of a user document in the MongoDB database. Key fields in the schema include _id, name, email, password, and role. The _id field is automatically generated using the uuid. Some fields have validation rules like required and enum.

### **Middleware for password hashing:**

The middleware function is registered using userSchema.pre('save', ...). This function is called before saving a user document to the database. It checks if the password field has been modified (e.g., during user registration or when changing a password) and then hashes the password using bcrypt.

### **Custom method for user model:**

The schema defines two custom methods that can be called on user documents:



* comparePassword: This method is used to compare a plaintext password (provided by a user during login) with the hashed password stored in the database. It uses bcrypt.compareSync for this purpose.
* jwtToken: This method generates a JSON Web Token (JWT) for the user, typically used for user authentication. It signs a payload containing the user's _id and sets an expiration time of 1 hour.

This model represents the "User" collection in the MongoDB database.

Finally, the User model is exported for use in other parts of the application. This allows other parts of the code to interact with the MongoDB database using the defined schema and model.

## **Create Joi schema for data validation**

We'll use Joi schema to validate the data our users send. We'll create a function that takes user data as a parameter for validation.


```
const Joi = require('joi')

const userSignUp = Joi.object({
  name: Joi.string().min(4).max(60).required(),
  email: Joi.string()
    .email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
    .required(),
  password: Joi.string()
    .pattern(
      new RegExp(
        /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!#.])[A-Za-z\d$@$#!%*?&.]{8,40}/
      ),
      {
        name: 'At least one uppercase, one lowercase, one special character, and minimum of 8 and maximum of 40 characters',
      }
    )
    .required(),
  role: Joi.string().valid('user', 'author', 'contributor').required(),
})

const loginUser = Joi.object({
  email: Joi.string()
    .email({ minDomainSegments: 2, tlds: { allow: ['com', 'net'] } })
    .required(),
  password: Joi.string()
    .pattern(
      new RegExp(
        /(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[$@$!#.])[A-Za-z\d$@$#!%*?&.]{8,40}/
      ),
      {
        name: 'At least one uppercase, one lowercase, one special character, and minimum of 8 and maximum of 40 characters',
      }
    )
    .required(),
})

exports.validateUserSignup = (data) => {
  const { err, value } = userSignUp.validateAsync(data)
  return { err: err, value }
}

exports.validateUserLogin = (data) => {
  const { err, value } = loginUser.validateAsync(data)
  return { err: err, value }
}
```


The above code defines two Joi validation schemas, userSignUp and loginUser, to validate user sign-up and login data. The validateUserSignup and validateUserLogin functions validate user input against these schemas and return validation results or errors.

## **Create auth middleware**

Middleware is a software/ piece of code that acts as a bridge between the database and the application, especially on a network. For the case of this project, we want to ensure that when a request is sent to the server, some code(middleware) is run before the request hits the server and returns a response. We want to check if a person who is trying to access a specific resource is authorized to access it.


```
// middleware/auth.js

const jwt = require('jsonwebtoken')
const User = require('../models/user.model')

exports.isAuthenticated = async (req, res, next) => {
  let token

  if (
    req.headers.authorization &&
    req.headers.authorization.startsWith('Bearer')
  ) {
    token = req.headers.authorization.split(' ')[1]
  }

  if (!token) {
    return res.status(401).json({ message: 'User not authorized' })
  }

  const decoded = jwt.verify(token, 'random string')
  req.user = await User.findById(decoded.id)

  next()
}

```


## **Create the controller**


```
// controllers/user.controller.js
const User = require('../models/user.model')
const {
  validateUserSignup,
  validateUserLogin,
} = require('../validators/user.validator')

exports.createUser = async (req, res) => {
  try {
    const { err } = validateUserSignup(req.body) // Validate the information from the request body
    if (err) return res.status(400).json({ message: err.message })
    const userExist = await User.findOne({ email: req.body.email }) // Checking if the user exist
    if (userExist) return res.status(400).json({ message: 'User exist' })
    const { name, email, password, role } = req.body
    const user = await User.create({ name, email, password, role }) //Creating the user
    if (!user) return res.status(400).json({ message: 'Cannot create user' })

    const token = await user.jwtToken()

    const options = {
      expiresIn: 3000,
      httpOnly: true,
    }

    return res.status(200).cookies('token', token, options).json({
      message: 'Signup successful',
      token,
    })
  } catch (error) {
    console.log('Unable to create a User')
  }
}

exports.loginUser = async (req, res) => {
  try {
    const { err } = validateUserLogin(req.body)
    if (err) return res.status(400).json({ message: err.message }) // Validate the users input

    // find the email of the user
    const user = await User.findOne({ email: req.body.email }).select(
      '+password'
    )
    // console.log(user)

    const isMatched = await user.comparePassword(req.body.password)
    if (!isMatched)
      return res.status(400).json({ message: 'Incorrect password or email' })

    const token = await user.jwtToken()

    const options = {
      httpOnly: true,
    }

    return res.status(200).cookie('token', token, options).json({
      message: 'Login successful',
      token,
    })
  } catch (error) {
    console.log(error.message)
  }
}

exports.userProfile = async (req, res, next) => {
  const user = await User.findById(req.user.id)
  if (!user) return res.status(200).json({ message: 'User not found' })
  return res.status(200).json({ message: 'Successfully', data: user })
}

exports.logOut = async (req, res) => {
  try {
    res.cookie('token', 'none', {
      expires: new Date(Date.now()),
    })
    return res
      .status(200)
      .json({ success: true, message: 'User is logout successfully' })
  } catch (error) {
    console.log(error.message)
  }
}
```


The above code actively handles user registration, login, user profile retrieval, and user logout. It incorporates input validation using Joi schemas and interacts with the "User" model and previously defined JWT-related functions.

## **Create the routes**

It’s finally time to create our different routes. Below is the list of endpoints we shall be creating.


```
const express = require('express')
const {
  createUser,
  loginUser,
  userProfile,
  logOut,
} = require('../controllers/user.controller')
const { isAuthenticated } = require('../middleware/auth')

const router = express.Router()

// Routes created

router.post('/register', createUser)
router.post('/login', loginUser)
router.get('/me', isAuthenticated, userProfile)
router.post('/logout', isAuthenticated, logOut)

module.exports = router
```


## **Serve the express application**


```
const express = require('express')
const { connectDB } = require('./config/database')
const dotenv = require('dotenv').config()
const cookieParser = require('cookie-parser')
const PORT = process.env.PORT

// Instantiating the mongodb database
connectDB()

// Instantiating the express application

const server = express()

server.get('/', (req, res) => {
  return res.json({
    message: 'This the Home page',
  })
})

// Importing our routes
const user = require('./routes/user.route')

// Express Inbuilt middleware

server.use(express.json()) // Used in passing application/json data
server.use(express.urlencoded({ extended: false })) // Used in passing form
server.use(cookieParser()) // Used in setting the cookies parser

// Routes for API

server.use('/api/v1', user)

// Creating the server
server.listen(PORT, () => console.log('Server is running on port ' + PORT))
```


Now you just need to run the project by using the following command

  <tr>
   <td>

```
npm run start
```


   </td>
  </tr>

## **Test with Postman**

Open Postman and create a post request to http://localhost:4000/api/v1/register as below:  


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/e2fbko7065roybyygjyz.png)

### **Create request**

Create a get request to http://localhost:4000/api/v1/me as below; Copy the jwt generated and paste in the authorization header to access the user profile


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/t5phlp8riby4yh04pt91.png)

### **Remove or delete the token from the header, you will get this:**

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tgpgonbl88hg0fni99ow.png)  

## **Conclusion**

This article covered JWT, authorization, authentication, and how to create an API in Node.js that uses JWT tokens for authentication.