Nurullah Türkoğlu

4 July 2024 · 4 min read

Authorization in Node.js With Express Middleware and JWT

How to handle authorization in a Node.js and Express API with JWT-checking middleware, including a user check and an admin role check on an endpoint.

Security matters more in web apps than it used to. Authorizing users and verifying their identity are two of the basic parts of any web application. On a platform like Node.js, middleware is an effective and efficient way to handle this. In this post I cover authorization with middleware in Node.js.

What is authorization, and why does it matter?

Authorization decides what a logged-in user is allowed to do: which rights they have, which pages they can open and which operations they can use. Every application has its own features. Some of them may be open only to an admin, others to moderators. You may also have normal members and premium members. In that case normal members should have certain restrictions, and they should not be able to reach the functions that premium members use.

Authorization vs. authentication

Authentication is about verifying identity. It checks whether the user's details are genuine. Email and password, username and password, or phone number verification on a login screen are good examples. Entering your card details for a credit card payment is an authentication step, and so is the SMS confirmation sent to your phone afterwards.

Authorization is about whether that user is allowed to do what they are trying to do. On an e-commerce site, for example, it covers whether a user can buy at a discount, whether they can edit other members' information, and which processes they can reach.

What is middleware?

Middleware is a layer of software between the request and the code that handles it, and each piece does one specific job. In Node.js, middleware functions run at a certain point in the request and response cycle and can be used while the request is processed. You usually chain several of them to get a modular, manageable application structure.

Using middleware in Node.js

Express.js is a minimal and flexible web framework that is widely used with Node.js. It makes middleware easy to use and gives your application a modular structure.

const express = require('express');
const app = express();

const myMiddleware = (req, res, next) => {
    console.log('Middleware ran!');
    next();
};

app.use(myMiddleware);

app.get('/', (req, res) => {
    res.send('Hello World!');
});

app.listen(3000, () => {
    console.log('Server is running!');
});

In this example a middleware called myMiddleware is defined and applied to all requests. The next() function makes the middleware pass control to the next one when it has finished its job.

Token-based authorization with JWT

JWT (JSON Web Token) is widely used for authentication and authorization in web applications. It is used to exchange data securely between the client and the server.

Roles and messages

// Define error messages as constants
const ERROR_MESSAGES = {
  NO_INPUT_TOKEN: "Unauthorized, there is no input token!",
  UNMATCHED_TOKEN: "Unauthorized, not matched token!",
  UNAUTHORIZED_LOGGED: "Unauthorized, you are not logged user.",
  UNAUTHORIZED_USER: "Unauthorized, you are not authorized.",
  USER_ID_NOT_FOUND: "User id not found in token.",
  // Add more error messages as needed
};

// Export the error messages
module.exports = ERROR_MESSAGES;
const roles = {
  CONTRIBUTOR: "contributor",
  ADMINISTRATOR: "administrator",
  SUBSCRIBER: "subscriber",
};

module.exports = roles;

Authorization middleware

First, a middleware for general user authorization.

const jwt = require("jsonwebtoken");
const ERROR_MESSAGES = require("../constants/messages");

const authMiddleware = (req, res, next) => {
  const authorizationHeader = req.header("Authorization");
  const userId = req.header("userId");

  if (!authorizationHeader || !userId) {
    return res.status(401).json({ msg: ERROR_MESSAGES.NO_INPUT_TOKEN });
  }

  const token = authorizationHeader.replace("Bearer ", "");

  jwt.verify(token, process.env.JWT_AUTH_SECRET_KEY, (err, decoded) => {
    if (err) {
      return res.status(401).json({ msg: ERROR_MESSAGES.UNMATCHED_TOKEN });
    }

    if (decoded.data.user.id === userId) {
      req.token = token;
      next();
    } else {
      return res.status(401).json({ msg: ERROR_MESSAGES.UNAUTHORIZED_LOGGED });
    }
  });
};

module.exports = authMiddleware;

This middleware uses JWT to verify the user's identity and authorize requests. It reads the user's userId and the Bearer token. The token is verified against the secret key we keep in our environment file. If verification succeeds, the id stored in the decoded data is compared with the userId the user sent in the header. That confirms the user's identity.

Admin authorization middleware

Next, a middleware for admin authorization.

const jwt = require("jsonwebtoken");
const ERROR_MESSAGES = require("../constants/messages");
const ROLES = require("../constants/roles");

const adminAuth = (req, res, next) => {
  const authorizationHeader = req.header("Authorization");

  if (!authorizationHeader) {
    return res.status(401).json({ msg: ERROR_MESSAGES.NO_INPUT_TOKEN });
  }

  const token = authorizationHeader.replace("Bearer ", "");

  if (token) {
    jwt.verify(token, process.env.JWT_AUTH_SECRET_KEY, (err, decoded) => {
      if (err) {
        res.status(401).json({ msg: ERROR_MESSAGES.UNMATCHED_TOKEN });
      } else {
        const roles = JSON.parse(decoded.data.user.roles);

        if (roles.includes(ROLES.ADMINISTRATOR)) {
          next();
        } else {
          res.status(401).json({ msg: ERROR_MESSAGES.UNAUTHORIZED_USER });
        }
      }
    });
  } else {
    res.status(401).json({ msg: ERROR_MESSAGES.NO_INPUT_TOKEN });
  }
};

module.exports = adminAuth;

This code checks whether the user has the admin role, so that only admins can reach the API endpoint we wrote. You can extend the function to make it dynamic, for example by turning it into a function that takes a role as a parameter and checks whether that role is present. Since this post keeps authorization simple, I picked a function that makes the purpose easy to follow.

Using middleware on an API endpoint

const userEtkinlikTableName = "UserEvent";

// @desc Get all user events
// @route /api/v1/user-event
// @access PUBLIC

router.get("/user-event", adminAuth, async (req, res) => {
  try {
    var sqlQuery = `SELECT UserId,EventId,EventName FROM ${userEventsTableName}`;

    db.query(sqlQuery, (err, data) => {
      if (err) throw err;
      res.status(200).json(data);
    });
  } catch (error) {
    res.status(500).json(error);
  }
});

This GET request is an endpoint that returns all user events stored in the table. We want only admins to reach it, so we pass the adminAuth middleware as the second parameter of router.get(). Before the request reaches the endpoint, we check that it comes from an admin. As in the middleware above, this uses JWT token verification.

Real-world examples

Banking systems: Money transfers are critical. What each user is allowed to do has to be defined and the system built around it. A logged-in user should only see the balances of their own accounts and should only have the access rights of their own membership type.

Social media platforms: A user should be able to edit only their own posts, not another account's. They should only comment with their own account, never in someone else's name.

Healthcare systems: Only doctors and authorized staff should be able to reach patients' records. A standard user should have no access to those files.

Common vulnerabilities and how to prevent them

Over-authorization means giving a user more rights than they need, so they can reach places they normally couldn't. To prevent it, list which roles may use each exposed API, and keep documentation that matches the list.

Weak session management can weaken authorization. Sessions that stay open forever, or are not ended properly, are examples that lead to vulnerabilities. To prevent this, analyze the system and decide how long sessions should stay open. The right value depends on how often the app performs transactions and how sensitive the user data is. Pick a suitable value based on that analysis.

Authentication weaknesses make it easier for attackers to get in without permission. Use strong password policies, multi-factor authentication (MFA) and secure password storage techniques.