RBAC (Role-Based Access Control) in Node.js
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)
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
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.
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 variableJWT_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 inverifyAuth
) 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
I hope this story helps you guys. Stay tuned and give it a clap if you want more content like this!
- cobanalitalha@gmail.com
- github.com/carpodok
- linkedin.com/alitalhacoban