RBAC (Role-Based Access Control) in Node.js

Ali Talha Çoban
9 min readOct 8, 2024

--

Hello fellows! In this article we’ll explore RBAC (Role-Based Access Control) in Node.js.

This story inludes;

  • Definition of RBAC (not the boring kind, with real-life examples)
  • Sample project with source code

Before you start, you can check the source code of this project from the link below.

You might also be interested in the following repo an API for a hospital app using RBAC with database management using MongoDB.

RBAC (Role-Based Access Control)

ref

Basically role-based access control is a method which allows us to perform some restrictions in projects. In other words, it is a security model that helps you manage user permissions in an organized way by assigning roles to users. Instead of granting permissions directly to each user, you group permissions into roles, and users are assigned those roles. Now that we’ve moved past the boring definition of RBAC, let’s grasp it better through a real-life hospital example.

Imagine running a hospital where different people (doctors, patients, and admin) need different levels of access. Doctors can update patient records, but patients can only view their own. That’s exactly how Role-Based Access Control (RBAC) works.

RBAC is like a security system that gives each person a badge with permissions based on their role. When a user logs in, the system checks their role(doctor, patient, or admin) and decides what they can access or do. It’s a simple way to make sure the right people have the right access, keeping everything secure and efficient.

RBAC Example Project

ref

In this project we’ll try to understand use of RBAC in real project. I’d like to point out that this project won’t include advanced use of RBAC, just for basic usage.

Here is the folder structure of this project;

Open the terminal and run the following commands to create a Node.js project

mkdir rbac-example && cd rbac-example

And then;

npm init -y

Run the command below and install all required dependencies;

npm i bcryptjs dotenv nodemon express jsonwebtoken

Last but not least, let’s configure package.json file as follows

 "scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node server.js",
"dev": "nodemon server.js"
},

Here we set the initialization method.

ref

Now we ready to go! Let’s crate our server.js file first.

const express = require("express");
const dotenv = require("dotenv");

dotenv.config();
const app = express();
app.use(express.json());

app.use("/", (req, res) => {
res.send("Hello RBAC Example!");
});

const PORT = process.env.PORT || 5000;

app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});

This is the basic server. To check if there is any problem, start the server

npm run dev

And go to http://localhost:5000 address in your browser. If there isn’t any problem you must see a message like below

Actually, there is a better way to test the endpoints, Postman. I’ve prepared a public collection for you guys. In this collection, all the endpoints you will use in the project are ready to use. Check here!

Mock Data

In this project, we’ll use mock data to test our routes. I’ll use the following data.js and fetch the data from this file. There are two types of data, users with their roles and resource to be manipulated.

Let’s create this file under utils folder.


const users = [
{ id: 1, username: "admin", password: "adminpass", role: "Admin" },
{ id: 2, username: "editor", password: "editorpass", role: "Editor" },
{ id: 3, username: "viewer", password: "viewerpass", role: "Viewer" },
];

const resources = [
{ id: 1, name: "Resource 1" },
{ id: 2, name: "Resource 2" },
{ id: 3, name: "Resource 3" },
];

module.exports = { users, resources };

Login Route

Let’s create our first route file which is auth.route.js. Since we already have a users data, we don’t need to register route for this project.

const express = require("express");
const jwt = require("jsonwebtoken");
const { users } = require("../utils/data");

const router = express.Router();

router.post("/login", (req, res) => {
const { username, password } = req.body;

const user = users.find(
(u) => u.username === username && u.password === password
);
if (!user) {
return res.status(404).json({ msg: "Invalid credentials" });
}

const payload = { id: user.id, role: user.role };
const token = jwt.sign(payload, process.env.JWT_SECRET, { expiresIn: "1h" });

return res.json({ token });
});

module.exports = router;

We define a login endpoint that verifies if the provided username and password exist in the system. If so, it generates a JWT token for the user and sends it as a response. This token is typically used to authenticate subsequent requests to protected routes in the application.

JWT is used to securely transmit information between parties as a JSON object. In this case, it carries the user’s id and role, allowing for token-based authentication in subsequent requests. We’l use it in the following sections.

Let’s start the server and test the route

If everthing is fine, we should get a response as follows

Resource

Now that the user login is ready, we can move on the resource part. Let’s start creating controller file named resource.controller.js under controllers folder as follows

const { resources } = require("../utils/data");

// Get all resources
const getResources = (req, res) => {
res.status(200).json(resources);
};

// Create a new resource
const createResource = (req, res) => {
const newResource = { id: resources.length + 1, name: req.body.name };
resources.push(newResource);
res.status(201).json({ msg: "Resource created", resource: newResource });
};

// Update an existing resource
const updateResource = (req, res) => {
const resource = resources.find((r) => r.id == req.params.id);
if (!resource) {
return res.status(404).json({ msg: "Resource not found" });
}
resource.name = req.body.name;
res.status(200).json({ msg: "Resource updated", resource });
};

// Delete a resource
const deleteResource = (req, res) => {
const index = resources.findIndex((r) => r.id == req.params.id);
if (index === -1) {
return res.status(404).json({ msg: "Resource not found" });
}
resources.splice(index, 1);
res.status(200).json({ msg: "Resource deleted" });
};

module.exports = {
getResources,
createResource,
updateResource,
deleteResource,
};

In this file we perform required CRUD operations for resource route. We’ll use these modules in resource.route.js, which is;

const express = require("express");
const {
getResources,
createResource,
updateResource,
deleteResource,
} = require("../controllers/resource.controller");

const router = express.Router();

// Get all resources
router.get(
"/",
getResources
);

// Create a new resource
router.post("/", createResource);

// Update an existing resource
router.put("/:id", updateResource);

// Delete a resource
router.delete("/:id", deleteResource);

module.exports = router;

We define our resource routes and assing controller modules to them. When we test this routes via Postman, we should get the following responses

If we get these responses that’s fine, maybe not…

We’re getting the response successfully, but is that how it’s supposed to be?No!

We’ve assigned different roles to users, so it’s essential to verify certain conditions, such as whether the user is logged in and if they have the necessary permissions to perform specific actions. We’ll utilize middlewares for this purposes

Middlewares

In short, middleware in Node.js are functions that handle requests between the client and the server’s final response. Think of it like airport security checks: each checkpoint, like passport control or baggage scanning, is a middleware function. It processes the passenger (request), deciding whether they can proceed to board the flight (response).

Similarly, in Node.js, middleware can authenticate users, log requests, or check permissions before sending a response, ensuring everything is in order before proceeding to the next step.

We build auth.js middleware file under middlewares folder as follows based on our purposes

const jwt = require("jsonwebtoken");

// Verify the token
const verifyAuth = (req, res, next) => {
const authHeader = req.header("Authorization");

if (!authHeader) {
return res
.status(401)
.json({ success: false, message: "No token provided" });
}

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

try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ msg: "Token is not valid" });
}
};

// Check if the user has the required role
const checkRole = (roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
msg: "You don't have permission to perform to do this operation!",
});
}
next();
};
};

module.exports = { verifyAuth, checkRole };

We define two middleware function in this file which are;

verifyAuth:

  • This function checks if the incoming HTTP request has an Authorization header containing a JSON Web Token (JWT).
  • If the header is missing, it responds with a 401 status and an error message indicating that no token was provided.
  • If the token is present, it removes the “Bearer “ part, leaving only the token.
  • The token is then verified using the jwt.verify method, with the secret stored in the environment variable JWT_SECRET.
  • If the token is valid, the decoded user data (from the token) is attached to the request object as req.user, and the request proceeds to the next middleware or route handler(resource.controller.js).
  • If the token is invalid, it responds with a 401 status and an error message stating the token is not valid.

checkRole:

  • This middleware function is used to restrict access based on user roles.
  • It takes an array of allowed roles as an argument.
  • It checks whether the user’s role (stored in req.user.role, which was decoded from the token in verifyAuth) is included in the allowed roles.
  • If the user’s role is not permitted, it responds with a 403 status and an error message.
  • If the user’s role is allowed, it proceeds to the next middleware or route handler.

Now we need to add this middleware functions to our resource.route.js.

const express = require("express");
const { verifyAuth, checkRole } = require("../middlewares/auth");
const {
getResources,
createResource,
updateResource,
deleteResource,
} = require("../controllers/resource.controller");

const router = express.Router();

// Get all resources
router.get(
"/",
verifyAuth,
checkRole(["Admin", "Editor", "Viewer"]),
getResources
);

// Create a new resource
router.post("/", verifyAuth, checkRole(["Admin", "Editor"]), createResource);

// Update an existing resource
router.put("/:id", verifyAuth, checkRole(["Admin", "Editor"]), updateResource);

// Delete a resource
router.delete("/:id", verifyAuth, checkRole(["Admin"]), deleteResource);

module.exports = router;

In updated version of resource.route.js ,first we check if the user is logged in. In this way, we can ensure if the user is not logged in, we don’t proceed to the next step in vain.

Our resource route is protected, so the user must be logged in before the perform any operation. Here is the response, if the user tries to get resources without logging in;

The next step is checking roles based on the resource operations. For instance, to delete any resource, we must be admin. That means that if someone other than the admin tries to delete a resource, we will not allow it. And the user will get an error as follows

--

--