Sunday, January 15, 2023

How to securely use JSON Web Tokens in a cookie with Node.js

Use JSON Web Tokens in a cookie with Node.js

 Despite the fact that JSON Web tokens (JWT) is a very popular authentication method and is loved by many. For most people, local storage is the best option.The intent of this article is not to create an argument about how to store the JWT in the frontend.

APIs could be consumed by a website, mobile application, or desktop application that you've developed.You will need to learn how to implement authentication and authorization in your API in a quick and easy way. The most popular approaches are cookies, sessions, and JWT.

What is a JWT?

In accordance with RFC 7519, JSON Web Tokens enables a secure connection between two parties. Its main purpose is to implement an authentication and authorization mechanism that is largely standardised in order to maximise interoperability.

A JWT consists of three parts. In a JWT, the first part is the header which describes the type of token along with its hashing algorithm. 

The second part, known as Payload, is the most fundamental part of every token, since it contains the information we added and that is relevant to us. 

The Secret Key, the Header, and the Payload make up the third and final part of a JWT.
Nevertheless, we can send the JWT in another way, as today we'll learn how to store it in a cookie.

Why use cookies?

Sometimes we don't want to send the JWT in the header when we make a request to the API. In this instance, cookies become useful. You can send them whenever an HTTP request is made without causing any problems.

Furthermore, if you use localstorage, on the frontend ensure that the user's JWT is removed from localstorage when they log out. Cookies can be removed from the front end with just an HTTP request. This is done by a route in the API.

The use of cookies is preferable for several reasons, here are some examples of small superficial situations that can occur while developing a project.

How to implement JWT in Node.js with cookies

The following dependencies need to be installed first:

npm install express jsonwebtoken cookie-parser

The next step is to create a simple API:

const express = require("express");

const app = express ();

app.get("/" (req, res) => {
    return res.json({message: "Hello world"})

});
const start = (port) => {
    try {
        app.listen(port, () => {
            console.log('My API is alive at http://localhost:${port};
        });
    } catch (error) {
        console.error(error);
        process.exit();
    }
};
start(3333);

We'll need something to allow us to work with cookies, which is why the cookie-parser is needed.

What is a Cookie-Parser?

The cookie header will be parsed and req.cookies will be populated with a keyed object based on the cookie names. 

As an option, you may enable signed cookie support by passing a secret string. This assigns req.secret  to the middleware, which it can use.

In order to register it in our middleware, we must first import it.

const express =require("express");
const cookieParser = require("cookie-parser");
const app = express();
app.use(cookieParser());

Let's start creating some routes in our API now.

The login will be our first route to create. Our jwt will be generated first, and then we will store it in the cookie "access_token". Several options will be available for the cookie, including httpOnly (for development purposes) and secure (for production purposes, using https.

A successful login will be announced via a reply:

app.get("/login" (req, res) => {
    const token = jwt.sign({id: 7, role: "captain"}, "YOUR_SECRET");
    return res     .cookie("access_toekn", token, {         httpOnly: true,         secure: process.env.NODE_ENV === "production",     })     .status(200)     .json({message: "Logged in. Nice work.});
});

The login has now been completed, let's see if our client received the cookie with the JWT.

Let's move on to authorisation now that authentication has been completed. This requires us to create a middleware in order to verify whether we have the cookie.

const authorization = (req, res, next) => {
    //Logic will go here
}
Our next step is to check whether we have our cookie, called "access_token", if not, then we will prevent access to the controller like this:
const authorization = (req, res, next) => {
  const token = req.cookies.access_token;
  if (!token) {
    return res.sendStatus(403);
  }
  //More logic will go here
};

Once the cookie has been received, the token will be verified in order to retrieve the data.Our system will, however, prevent access to the controller if an error occurs.

const authorization = (req, res, next) => {
  const token = req.cookies.access_token;
  if (!token) {
    return res.sendStatus(403);
  }
  try {
    const data = jwt.verify(token, "YOUR_SECRET_KEY");
    //Things will be added here soon
  } catch {
    return res.sendStatus(403);
  }
};

It is time to add new properties to the request object so that we can access the token's information more easily. This will be accomplished by creating the req.userId and assigning it the token value. A req.userRole  will also be created and assigned the value it contains. Afterward, just grant your controller access.

const authorization = (req, res, next) => {
  const token = req.cookies.access_token;
  if (!token) {
    return res.sendStatus(403);
  }
  try {
    const data = jwt.verify(token, "YOUR_SECRET_KEY");
    req.userId = data.id;
    req.userRole = data.role;
    return next();
  } catch {
    return res.sendStatus(403);
  }
};

Our next step is to create a new route, the route to log out. Our goal is to remove the value from the cookie. This means that we will remove the JWT.

To make our new route more robust, however, we want to add authorization middleware. The reason is that if the cookie is present, we will log out the user. We will remove the cookie value and send the user a message indicating that they have successfully logged out.

app.get("/logout", authorization, (req, res) => {
    return res
    .clearCookie("access_toekn")
        .status(200)
        .json({message: "You're now logged out.});
});

It is now time to test whether we can log out. The purpose is to verify that we receive a successful message when we log out for the first time. If we test again without the cookie, we should get a forbidden error message.

There is one last route we need to create so that we can access the data from the JWT. In order to access this route, we need to have access to the JWT in the cookie. Otherwise, we will receive an error. As a result, the request's new properties can now be used.

app.get("/protected", authorization, (req, res) => {
    return res.json({ user: { id: req.userId, role: req.userRole } });
});

Before implementing the entire workflow, we will test it using our favourite client. Remember these key considerations:

  • The cookie can be obtained by logging in;
  • For access to the JWT data, visit the protected route;
  • The cookie can be cleared by logging out;
  • You should receive an error message if you visit the protected route again.

How to test security controls in your Node/Express app?

The first step is simple and it's free. Use this free HTTP header scanning service to understand if you have correctly implemented the CSP header on your web server.

The advanced next step in protecting your Node.js application and APIs against hackers is to run regular vulnerability scans with a user-friendly web application vulnerability scanning tool, like Cyber Chief.

Start your free trial of Cyber Chief now to see not only how it can help to keep attackers out, but also to see how you can ensure that you ship every release with zero known vulnerabilities. 

Or, if you prefer to have an expert-vetted vulnerability assessment performed on your Node.js application you can order the vulnerability assessment from here. Each vulnerability assessment report comes with:

  • Results from scanning your application for the presence of OWASP Top 10 + SANS CWE 25 + thousands of other vulnerabilities.
  • A detailed description of the vulnerabilities found.
  • A risk level for each vulnerability, so you know which ones to fix first.
  • Best-practice fixes for each vulnerability, including code snippets where relevant.
  • One-month free access to our Cyber Chief web application security testing & vulnerability management tool.
  • Email support from our application security experts.

Which option do you prefer?