Authentication and Authorization in Node.js: A Comprehensive Guide to JWT

Authentication and Authorization in Node.js: A Comprehensive Guide to JWT

·

12 min read

Welcome to our blog post on authentication and authorization in Node.js using JSON Web Tokens (JWT). Whether you're a Node.js developer, this guide will provide you with everything you need to know about implementing secure authentication and authorization in your Node.js applications.

Introduction

Authentication and authorization are fundamental aspects of any secure web application. In simple terms, authentication is the process of verifying the identity of a user, while authorization determines what actions a user is allowed to perform. JWT, or JSON Web Tokens, is a popular and secure method for implementing authentication and authorization in Node.js applications.

In this blog post, we will delve into the intricacies of JWT and explore how it can be utilized to enhance the security of your Node.js applications. We will cover everything from understanding the basics of JWT to implementing it in a practical scenario. So, let's get started!

What is JWT?

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. It consists of three parts: the header, payload, and signature.

The Header

The header of a JWT contains metadata about the type of token and the algorithm used to sign it. The most commonly used algorithm is HMAC SHA256 or RSA, but other algorithms can also be used. The header is Base64Url encoded to form the first part of the token.

The Payload

The payload contains the claims, which are statements about an entity (typically, the user) and additional metadata. Claims can be divided into three types: registered claims, public claims, and private claims. The payload is also Base64Url encoded and forms the second part of the token.

The Signature

The signature is created by combining the encoded header, encoded payload, a secret key, and the algorithm specified in the header. It is used to verify the authenticity of the token and to ensure that its contents have not been tampered with.

Why Use JWT?

Now that we have a basic understanding of JWT, let's explore why it is widely used for authentication and authorization in Node.js applications.

Stateless Authentication

JWT allows for stateless authentication, meaning that the server-side application does not need to maintain a session in memory for each authenticated user. This makes it highly scalable and ideal for distributed systems.

Cross-Domain Support

Because JWTs are self-contained, they can be easily transmitted between different domains. This allows for seamless communication between various components of a distributed system without the need for complex session management.

Enhanced Security

JWTs are digitally signed, ensuring that the contents of the token cannot be modified without detection. Additionally, JWTs can be encrypted to further enhance security. This makes JWT a robust solution for securing web APIs and microservices.

Easy Integration and Interoperability

JWT is widely supported across various programming languages and platforms, making it easy to integrate with existing systems and libraries. This interoperability ensures that a JWT generated by a Node.js application can be consumed by a client application written in a different language or framework.

Implementing JWT in Node.js

Now that we understand the basics of JWT and its advantages, let's dive into the practical implementation of JWT in a Node.js application. We will go through each step in detail, providing insights, tips, and explanations along the way.

Prerequisites

Before we get started, certain prerequisites need to be installed on your system. These include:

  • Node.js v19.6.0

  • NPM (Node Package Manager) 9.4.0

  • MongoDB 5.0.14

  • Express ^4.18.2

  • Mongoose ^7.3.1

  • body-parser

  • jsonwebtoken

  • bcryptjs

Step 1: Setting Up a Node.js Project

Before we begin implementing JWT, we need to set up a basic Node.js project. This can be done by following these steps:

  1. Create a new directory for your project.

  2. Open a terminal and navigate to the newly created directory.

  3. Run the following command to initialize a new Node.js project:

npm init

  1. Follow the prompt to provide basic information about your project.

  2. Once the initialization is complete, you will have a package.json file in your project directory.

Step 2: Installing Required Dependencies

To work with JWT in Node.js, we need to install the necessary dependencies. Run the following command in your project directory to install the dependencies:

npm install jsonwebtoken express body-parser mongoose bcryptjs

Let's go through what each of these dependencies does:

  • jsonwebtoken: This is the main JWT library for Node.js that provides functions for generating, signing, and verifying JWTs.

  • express: Express is a popular web application framework for Node.js that provides a robust set of features for building APIs and web applications.

  • body-parser: Body-parser is a middleware for Express that parses incoming request bodies. We will use it to extract the JWT from the Authorization header.

  • mongoose: Mongoose is an Object Data Modeling (ODM) library for MongoDB and Node.js. It provides a straightforward, schema-based solution for modeling your application data and interacting with MongoDB databases.

  • bcryptjs: bcrypt.js is a JavaScript library used for hashing and salting passwords. It is particularly popular in Node.js applications and is used to enhance the security of user authentication systems by storing passwords securely

Step 3: Setting Up a Basic Express Server

Now that we have installed the required dependencies, let's set up a basic Express server to handle incoming requests. Create a new file called app.js in your project directory and add the following code:

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

/* Configure body-parser middleware */
app.use(bodyParser.json({ limit: '50mb' }));


/* Start the server with port 3000 */
app.listen(3000, () => console.log('Example app is listening on port 3000.'));

This code sets up a basic Express server that listens on port 3000 and uses the body-parser middleware to parse incoming JSON requests. You can start the server by running node server.js in your terminal.

Step 4: Connecting to MongoDB

The next step is to connect to our MongoDB database. We can do this by using the Mongoose library. Mongoose provides a simple and elegant API for working with MongoDB. To connect to our database, we need to define a Mongoose connection object in our app.js file:

const mongoose = require('mongoose');
mongoose.connect('mongodb://localhost/node-auth');

Step 5: Creating a Model

Before we can perform operations in MongoDB, we need to define a data model. A data model represents the structure and properties of the data that we want to store. In our case, we will define a simple data model to store information about users. Here’s how we can create a user model:

Create a models folder and within that folder create a file called users.js and add the below code to that file

const mongoose = require('mongoose');
const Schema = mongoose.Schema;

const UserSchema = new Schema({
    name: String,
    email: String,
    password: String,
    age: Number
});

const UserModel = mongoose.model('User', UserSchema);

module.exports = UserModel;

Step 4: User Registration

Instead of a registration form, design the API endpoints that will handle registration requests. We'll define the routes and HTTP methods (usually POST) required to process user registration data.

Handling Registration Requests: In Node.js and Express, create route handlers for processing registration requests. When a client sends a POST request with user registration data, your API will validate and process the data.

Let's create a User Registration API

const Users = require('./models/users');
const bcrypt = require('bcryptjs');

/* create user */
app.post('/Signup', async (req, res) => {

    try {

        /* Note this is Basic Simple Registration api can't be used directly in real time Projects */

        const { name, email, password } = req.body /* destructuring the field from req.body */

        const salt = await bcrypt.genSalt(10);
        const hashPassword = await bcrypt.hash(password.trim(), salt);  /* encrypt password */

        const user = new Users({
            email: email,
            name: name,
            password: hashPassword,
        });

        const result = await Users.create(user) /* storing user details in database with help of mongoose */

        if (result) return res.status(200).send({ success: true, message: 'Created successfully', data: result })
        else return res.status(400).send({ success: false, message: 'Failed', })
    } catch (error) {
        return res.status(500).send({ success: false, message: 'Something went wrong', })

    }
});

To test this endpoint, you can use tools like cURL or Postman. Send a POST request to http://localhost:3000/signup, below are the results for signup API:

Step 5: Generating and Verifying JWTs

Now that we have our basic server set up with Registration API, let's move on to generating and verifying JWTs. JWTs are typically issued when a user successfully logs in to the application. To demonstrate this, let's create a simple /login endpoint that returns a JWT when provided with valid credentials.

Add the following code to your app.js file:

const jwt = require('jsonwebtoken');

/* login api */
app.post('/login', async (req, res) => {
    try {

        const { email, password } = req.body

        /* find user based on email */
        const user = await Users.findOne({ email: email });

        /* if no user found with provided email id return error response */
        if (!user) {
            return res.status(401).send({ status: 401, success: false, message: 'Unauthorized' });
        }

        /* generate jwt token with user id payload */
        const token = jwt.sign({ user: user._id }, 'secret-key');

        return res.status(200).send({ success: true, message: 'Logged in successfully', data: token })

    } catch (error) {
        return res.status(500).send({ success: false, message: 'Something went wrong', })
    }

})

In this code, we define a /login endpoint that accepts a POST request containing the email and password. We then search for a user with the provided credentials in the users Collection. If a user is found, we generate a JWT with the user's ID as the payload using the jsonwebtoken library. Finally, we return the generated JWT as the response.

To test this endpoint, you can use tools like cURL or Postman. Send a POST request to http://localhost:3000/login with the following payload, below are the results:

Congratulations! You have successfully generated a JWT after a user logs in.

Step 6: Protecting Routes with Authorization Middleware

Now that we can generate JWTs, let's move on to protecting routes in our application. We will create a simple /protected endpoint that can only be accessed by authenticated users with a valid JWT.

Add the following code to your app.js file:

/* verify token middleware */
const verifyToken = (req, res, next) => {
    try {

        const { authorization } = req.headers;

        // Check if the Authorization header exists

        if (!authorization) {
            return res.status(401).send({ status: 401, success: false, message: 'Please add token in header' });
        }

        const token = authorization.replace('Bearer ', '');

        /* jwt.verify can compare the toekn with secret and if all good it will return the payload */
        jwt.verify(token, 'secret-key', async (err, payload) => {
            if (err) {
                return res.status(401).json({ status: 401, success: false, message: 'You must be logged in ' });
            }
            const { user } = payload;
            const getUserInfo = await Users.findById(user);
            if (getUserInfo) {
                req.user = getUserInfo;
                next();
            } else {
                return res.status(401).json({ status: 401, success: false, message: 'Un authorized' });
            }
        });

    } catch (error) {
        return res.status(500).send({ success: false, message: 'Something went wrong', })
    }
};

// Protected endpoint

/* Protected Route */
app.get('/users', verifyToken, (req, res) => {
    try {

        /* sample response if you are authorized user you will get this response */
        res.status(200).send({ success: true, message: 'Reponse from Protected endpoint', })

    } catch (error) {
        return res.status(500).send({ success: false, message: 'Something went wrong', })
    }

})

In this code, we define a verifyToken middleware function that checks if the Authorization header exists in the request. If the header is absent, we return a 401 Unauthorized response. If the header is present, we then attempt to verify the JWT and extract the payload using the jsonwebtoken library. If the verification is successful, we attach the payload to the request object (req.user) and proceed to the next middleware. Otherwise, we return a 401 Invalid token response.

We then create a /protected endpoint that uses the verifyToken middleware. If a request is made to this endpoint with a valid JWT, the server will respond with a JSON object containing a message and the user's ID from the JWT payload.

To test this endpoint, send a GET request to http://localhost:3000/users with the Authorization header set to Bearer <JWT>, where <JWT> is the JWT obtained from the /login endpoint. If the JWT is valid, you should receive the following response:

Step 7: Token Expiration and Refresh Tokens

In real-world applications, JWTs often have an expiration time to limit their validity. This helps mitigate the risk of long-lived tokens being compromised. Additionally, a refresh token mechanism can be implemented to allow users to obtain new JWTs without re-entering their credentials.

To implement token expiration and refresh tokens, we need to make a few modifications to our previous code.

First, let's update the login endpoint to include an expiration time for the generated token:

 /* generate jwt token with user id payload  the token will be expired in one hour*/
 const token = jwt.sign({ user: user._id }, 'secret-key', { expiresIn: '1h', });

In this example, we set the expiration time to 1 hour. Change this value to fit your application's requirements.

Next, let's create a new /refresh endpoint that accepts a refresh token and returns a new JWT:

// Refresh endpoint

app.post('/refresh', (req, res) => {
    try {

        const { authorization } = req.headers

        /* verify the refresh token */
        const payload = jwt.verify(authorization, 'secret-key');


        // Generate a new JWT
        const token = jwt.sign({ user: payload.user }, 'secret-key', {

            expiresIn: '1h',

        });

        // Return the new JWT as a response

        return res.status(200).send({ success: true, message: 'Refresh token', data: token })

    } catch (error) {
        console.log(error);
        return res.status(500).send({ success: false, message: 'Something went wrong', })
    }
})

In this code, we define a /refresh endpoint that accepts a POST request with a refreshToken in the request body. If the verification is successful, we generate a new JWT with the user ID from the refresh token payload and an expiration time. Finally, we return the new JWT as the response.

To test the refresh token mechanism, you need to generate a refresh token and send a POST request to http://localhost:3000/refresh with the following payload:

If the refresh token is valid, you should receive a response containing a new JWT.

Full Source Code:

https://github.com/dev-vasanth/Authentication-Authorization-nodejs

Conclusion

Congratulations on completing this comprehensive guide to authentication and authorization in Node.js using JWT! We have covered the basics of JWT, its advantages, and practical implementation steps in a Node.js application. By following this guide, you now have the knowledge and tools to implement secure authentication and authorization in your own Node.js applications.

Remember to always prioritize security in your applications and stay informed about best practices for handling user authentication and authorization. Additionally, consider exploring other security measures such as rate limiting, input validation, and encryption to further enhance the security of your applications.

We hope you found this guide informative and practical. If you have any questions or feedback, feel free to leave a comment below. Happy coding!

Did you find this article valuable?

Support Vasanth by becoming a sponsor. Any amount is appreciated!