JSON web token (JWT) authentication in NodeJS applications

A JSON Web Token, popularly known as JWT, is an open standard that defines a compact way for securely sharing information between two parties: a client and a server.

Unlike the traditional server-side sessions — which save a session id in memory and return it to the client — this standard creates a self-contained and digitally signed token that is verified each time a request is made.

In this article, you will learn how to add a layer of security to your application by using JWTs to authenticate requests made to your application.

Structure of a JWT

A JWT consists of a header, a payload, and a signature.

The header contains metadata about the token type and the type of algorithm it is secured with. The type of token, which is a JWT, and the signing algorithm being used, which can either be a hash-based message authentication code (HMAC), a secure hash algorithm (SHA256), or a Rivest-Shamir-Adleman (RSA) algorithm.

For example:

{
  "alg": "HS256",
  "typ": "JWT"
}

The payload contains verifiable statements about users, such as their identity and access permissions, known as claims:

{
  "email": "[email protected]",
  "iat": 1667134212,
  "exp": 1667136012
}

The signature is a string generated from the signing algorithm used to verify that the received JWT has not been tampered with. When working with JWTs, you must check its signature before storing and using them:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload), secret)

The three parts of the JWT are concatenated, separated by a dot (.) and encoded in Base64Url to form the JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZpcnN0QGdtYWlsLmNvbSIsImlhdCI6MTY2NzEzNDIxMiwiZXhwIjoxNjY3MTM2MDEyfQ.55_3rFnl2KMQ0tRLusWY6EExHTGjVSbNPX8VFMSQTlQ

Setting up your development environment

To follow this tutorial, you will need to clone this GitHub repository, which contains boilerplate code for a simple user-authentication model.

After cloning the repository, run the following command to install all the dependencies:

npm install

The dependencies installed include:

Note: To follow along with this tutorial, you’ll need to connect your application to a MongoDB database. If you have Docker installed, you can use the docker-compose.yml included with docker compose up. Just be sure to change the left side of the volume path (i.e., /Users/andrew/techne/node-jwts/mongo) to be something on your local drive. It will load a local database for you in Docker corresponding to MONGODB_URI=mongodb://mongo:mongo@localhost:27017 in your .env file.

Creating a JSON web token

First, open your userRoute.js file and import jsonwebtoken into it like so:

const jwt = require("jsonwebtoken");

Next, you need to create a JWT and assign it to a user when they successfully login into your application.

You can create and sign a JWT using the jsonwebtoken.sign() method. This method takes three arguments, a payload, a token secret, and a configuration object.

The payload can be user data, such as the username or email.

The token secret is a random string used to encrypt and decrypt data. This string should be as long and as random as possible to make the authorization process harder for malicious users.

To ensure the string is random and unique, use the inbuilt Node.js crypto module to generate it. To achieve this, open a new terminal and run the command below:

node

The command in the code block above allows you to write Node.js in your terminal.

Next, run the following command in your node terminal:

>require("crypto").randomBytes(32).toString("hex") 
//'4c0d608098b78d61cf5654965dab8b53632bf831dc6b43f29289411376ac107b'

The command above generates a 32-byte string.

After that, copy the generated string and add it to a .env file like this:

//.env
JWT_SECRET=4c0d608098b78d61cf5654965dab8b53632bf831dc6b43f29289411376ac107b

Next, in your sign-in route handler, call the jwt‘s sign method and pass in the payload (email), the secret, and the expiry. Store the result in a variable like so:

const token = jwt.sign({email}, process.env.JWT_SECRET, { expiresIn: "1800s" })

        return res
          .status(200)
          .json({ message: "User Logged in Successfully", token });

Note: You can only set an expiry if the payload is an object literal.

Authenticating a JSON web token

Using Express’s middleware functionality, you can create a middleware that protects routes from unauthorized access by checking and authenticating a JWT.

Add the code block below to your userRoute.js file to create the authentication middleware:

const authenticateToken = (req, res, next) => {
  const authHeader = req.headers["authorization"];

  //Extracting token from authorization header
  const token = authHeader && authHeader.split(" ")[1];

  //Checking if the token is null
  if (!token) {
    return res.status(401).send("Authorization failed. No access token.");
  }

  //Verifying if the token is valid.
  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      console.log(err);
      return res.status(403).send("Could not verify token");
    }
    req.user = user;
  });
  next();
};

In the authenticateToken middleware above, you first check for an authorization header. Then, you extract the token from the authorization headers if it exists by turning the authorization header to an array and taking the element at index 1. The authorization headers come in a “Bearer token“ format, i.e., the authorization header would be in this format: “Bearer eyxxx.tyxxxx.zyxxx”, which is why splitting it into an array and taking the element at index 1 gives you the token. Then, you check if the token exists and throw an error if it doesn’t.

Finally, you verify the token’s authenticity using the jwt.verify() method. The verify method takes the extracted token, your JWT secret, and a callback as arguments. Once the token is verified, save the user’s details to the request object to keep track of the user’s activities.

You can apply this middleware to all routes you want to protect from unauthorized access:

router.get("/test", authenticateToken, (req, res) => {
  res.send("User authorized");
});

Testing protected routes

To test if your protected routes are actually only accessible to authorized users, you can use Postman or any other API client to make requests to the routes. This tutorial will feature the use of Postman. First, add the code block below to your userRoutes.js file:

router.get("/test", authenticateToken, (req, res) => {
  res.send("Token Verified, Authorizing User...");
});

The route above is protected by the authenticateToken middleware you created earlier, which implies that the route cannot be accessed without an access token. Then bring your application online with the node app.js command.

Next, on Postman, try to access the protected route (https://localhost:3000/test) without logging into your application to retrieve the token. You should get a 401 Unauthorized response, as shown in the image below:

401 unauthorized response

Next, create a new user by making a POST request to https://localhost:3000/sign-up with the fake data below or any data of your choice:

{
    "email": "[email protected]",
    "password": "password"
}

Now, log into your application using the details you created the user with. After you log in successfully, you will receive the access token as part of the response, as shown in the image below:

jwt authentication access tokens

Copy the access token, add it to the authorization header under Bearer Token, and make another request to your protected route (https://localhost/3000/test). If the token is verified, you’ll get a 200 OK response, as shown in the image below:

jwt access token verification

Implementing refresh tokens

When a user’s access token expires, the user has to be re-authenticated by the application to obtain a new access token. The re-authentication can be skipped using refresh tokens, which are tokens used to generate a new access token.

To generate a refresh token, you first have to generate a refresh token secret. You can follow the same steps used to generate a JWT secret to generate a refresh token secret:

>require("crypto").randomBytes(32).toString("hex") 
//'c4a71ea639f6de11f146f21193d91c4518a3c285f1853d44778910d6130a4c03'

Next, copy the generated string and add it to your .env file:

//.env
REFRESH_TOKEN_SECRET=c4a71ea639f6de11f146f21193d91c4518a3c285f1853d44778910d6130a4c03

Then, in userRoutes.js, create an in-memory storage like an array for the refresh tokens:

const refreshTokens = []

Next, update your /sign-in route to generate a refresh token along with the JWT access token and save the generated refresh token to the array you created earlier:

const token = jwt.sign({email}, process.env.JWT_SECRET, { expiresIn: "1800s" })
        const refreshToken = jwt.sign(email, process.env.REFRESH_TOKEN_SECRET);
        refreshTokens.push(refreshToken);

        return res.status(200).json({
          message: "User Logged in Successfully",
          token,
          refreshToken,
        });

Your finished /sign-in route handler should look like this:

router.post("/sign-in", async (req, res) => {
  try {
    //Extracting email and password from the req.body object
    const { email, password } = req.body;

    //Checking if user exists in database
    let user = await User.findOne({ email });

    if (!user) {
      return res.status(401).json({ message: "Invalid Credentials" });
    }

    //Comparing provided password with password retrieved from database
    bcrypt.compare(password, user.password, (err, result) => {
      if (result) {
        const token = jwt.sign({email}, process.env.JWT_SECRET, { expiresIn: "1800s" })
        const refreshToken = jwt.sign(email, process.env.REFRESH_TOKEN_SECRET);
        refreshTokens.push(refreshToken);

        return res.status(200).json({
          message: "User Logged in Successfully",
          token,
          refreshToken,
        });
      }

      console.log(err);
      return res.status(401).json({ message: "Invalid Credentials" });
    });

  } catch (error) {
    res.status(401).send(err.message);
  }
});

Next, create a new route handler for /token that handles the new access token creation:

router.post("/token", (req, res) => {
  const refreshToken = req.body.token;
  if (refreshToken == null) {
    return res.status(401).send("No refresh token provided!");
  }
  if (!refreshTokens.includes(refreshToken)) {
    return res.status(403).send("Invalid Refresh Token");
  }

  jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET, (err, user) => {
    if (err) return res.status(403).send("Could not Verify Refresh Token");

    const token = jwt.sign({email: req.body.email}, process.env.JWT_SECRET, { expiresIn: "1800s" })
    res.json({ accessToken: token });
  });
});

In the code block above, you first extract the refresh token from the body object. Then, check if the refresh token is null and send a 401 status code if it is. After that, check if the refresh token is from your application by seeing if it’s stored in your array.

Finally, check if the refresh token is valid using the verify method. If the token is valid, create a new access token and return it to the user, else you send a 403 status code.

Conclusion

This tutorial covered how you can add an extra layer of security to your application using JWTs. 

Although JWTs are not easily exploitable, they can be exploited if the tokens are stored carelessly in local storage, which can be very hard to revoke. Be sure to be careful when working with these tokens.

Happy coding! If you like content like this, browse the Mattermost Library for more tips and tricks on developing software using open source technologies.

This blog post was created as part of the Mattermost Community Writing Program and is published under the CC BY-NC-SA 4.0 license. To learn more about the Mattermost Community Writing Program, check this out.

Read more about:

nodejs security

David is a software developer and technical writer with experience building scalable backend infrastructure for web applications and writing various in-depth articles related to backend-focused software engineering for companies such as Digital Ocean, Fixate.io, and Draft.dev, to mention a few. He is primarily interested in exploring and contributing to emerging technology.