Building Role-Based Access Control (RBAC) in Node.Js and Express.Js

Jayant Choudhary
6 min readDec 21, 2023

Role-Based Access Control (RBAC) stands as a pivotal pillar in ensuring robust application security. Offering a systematic framework, RBAC empowers organisations and applications to meticulously govern and limit resource access in accordance with users’ designated roles.

This in-depth guide delves into the core principles of RBAC, elucidates its advantages, and provides a step-by-step walkthrough for integrating RBAC seamlessly into a Node.js application.

RBAC

Project Structure

Below is the fundamental project structure that will serve as the foundation for this tutorial:

rbac-project/

├── package.json
├── package-lock.json
├── server.js
|
├── models/
│ └── user.js
|
├── routes/
│ └── auth.js
│ └── records.js
|
├── controllers/
│ └── authController.js
│ └── recordsController.js
|
├── middleware/
│ └── rbacMiddleware.js

└── config/
└── roles.json

Defining Roles and Permissions

Let’s start by defining some roles and permissions for our rbac project.

Create a roles.json file in the config/ directory:

{
"roles": [
{
"name": "admin",
"permissions": [
"create_record",
"read_record",
"update_record",
"delete_record"
]
},
{
"name": "manager",
"permissions": [
"create_record",
"read_record",
"update_record"
]
},
{
"name": "employee",
"permissions": [
"create_record",
"read_record"
]
}
]
}

Here, we’ve defined three roles: “admin,” “manager,” and “employee,” each with different sets of permissions related to task management.

Storing Role and Permission Data

Now, let’s create models to represent roles and permissions. In the models/ directory, create a file named role.js:

// models/role.js

const roles = require('../config/roles.json');

class Role {
constructor() {
this.roles = roles.roles;
}

getRoleByName(name) {
return this.roles.find((role) => role.name === name);
}

getRoles() {
return this.roles;
}
}

module.exports = Role;

We’ve created a Role class that reads the roles and permissions from the roles.json file. We also provide methods to retrieve roles by name or get a list of all roles.

Next, create a permissions.js file in the same models/ directory:

// models/permissions.js

class Permissions {
constructor() {
this.permissions = [];
}

getPermissionsByRoleName(roleName) {
const role = roles.roles.find((r) => r.name === roleName);
return role ? role.permissions : [];
}
}
module.exports = Permissions;

The Permissions class allows us to fetch permissions based on a role's name.

We'll use these models when assigning roles to users and checking permissions in the RBAC middleware.

Implementing User Authentication

User authentication is a fundamental part of RBAC. We’ll use a popular Node.js library called “Passport” to handle authentication. Here’s how you can set up Passport for your Node.js application:

First, install the required dependencies:

npm install mongoose passport passport-local passport-local-mongoose express-session

Now, create a user model using Mongoose and Passport in the models/user.js file:

// models/user.js

const mongoose = require('mongoose');
const passportLocalMongoose = require('passport-local-mongoose');

const userSchema = new mongoose.Schema({
username: String,
password: String,
role: String,
});

userSchema.plugin(passportLocalMongoose);

const User = mongoose.model('User', userSchema);

module.exports = User;

Utilised passport-local-mongoose for user authentication.

Role Assignment

Now, when a user registers or is created, we need to assign a role to them. In the controllers/authController.js file, add the following code:

// controllers/authController.js

const User = require('../models/user');
const Role = require('../models/role');

// Register a new user with a role
exports.registerUser = (req, res) => {
const { username, password, role } = req.body;
const user = new User({ username, role });

User.register(user, password, (err) => {
if (err) {
console.log(err);
return res.status(500).json({ error: err.message });
}
res.json({ message: 'User registered successfully' });
});
};

In the above code, we create a new user and specify their role during registration.

Creating RBAC Middleware

Middleware in Node.js is essential for handling tasks such as route protection and user authorization. We’ll create RBAC middleware to check if a user has the necessary permissions to access a specific route.

In the middleware/rbacMiddleware.js file, add the following code:

// middleware/rbacMiddleware.js

const Role = require('../models/role');
const Permissions = require('../models/permissions');

// Check if the user has the required permission for a route
exports.checkPermission = (permission) => {
return (req, res, next) => {
const userRole = req.user ? req.user.role : 'anonymous';
const userPermissions = new Permissions().getPermissionsByRoleName(userRole);

if (userPermissions.includes(permission)) {
return next();
} else {
return res.status(403).json({ error: 'Access denied' });
}
};
};

In this middleware, we extract the user’s role from their session and check if the role has the required permission to access a route. If the user has the necessary permission, they are allowed to proceed; otherwise, a “403 Forbidden” response is sent.

Protecting Routes

To protect specific routes using the RBAC middleware, import the middleware function and apply it to the desired routes in your route handlers.

Here’s an example of how to protect a route using the checkPermission middleware:

// routes/records.js

const express = require('express');
const router = express.Router();
const rbacMiddleware = require('../middleware/rbacMiddleware');

// Import your controller
const recordsController = require('../controllers/recordsController');

// Protect the route with RBAC middleware
router.get('/records', rbacMiddleware.checkPermission('read_record'), recordsController.getAllRecords);

module.exports = router;

Now, the /tasks route is protected, and only users with the "read_task" permission will be able to access it.

Building a Simple Records Management System

Let’s start by defining the basic routes for our Record Management System. Create a new file named records.js in the routes/ directory:

// routes/records.js

const express = require('express');
const router = express.Router();
const rbacMiddleware = require('../middleware/rbacMiddleware');

// Import your controller
const recordsController = require('../controllers/recordsController');

// Protect the routes with RBAC middleware
router.get('/records', rbacMiddleware.checkPermission('read_record'), recordsController.getAllRecord);
router.post('/records', rbacMiddleware.checkPermission('create_record'), recordsController.createRecord);
router.put('/records/:id', rbacMiddleware.checkPermission('update_record'), recordsController.updateRecord);
router.delete('/records/:id', rbacMiddleware.checkPermission('delete_record'), recordsController.deleteRecord);

module.exports = router;

In this code, we have defined routes for listing, creating, updating, and deleting records. Each route is protected by the RBAC middleware, which checks the user’s permissions before allowing access.

Defining Roles and Permissions

Before we can assign roles to users, let’s define the roles and permissions for our application. In the config/roles.json file, specify the roles and their associated permissions:

{
"roles": [
{
"name": "admin",
"permissions": ["create_record", "read_record", "update_record", "delete_record"]
},
{
"name": "manager",
"permissions": ["create_record", "read_record", "update_record"]
},
{
"name": "employee",
"permissions": ["create_record", "read_record"]
}
]
}

We’ve defined three roles: “admin,” “manager,” and “employee,” each with different levels of access.

Implementing Authentication and Authorization

In the controllers/authController.js file, implement user registration and login functionalities:

// controllers/authController.js

const User = require('../models/user');
const Role = require('../models/role');

// Register a new user with a role
exports.registerUser = (req, res) => {
const { username, password, role } = req.body;
const user = new User({ username, role });

User.register(user, password, (err) => {
if (err) {
console.error(err);
return res.status(500).json({ error: err.message });
}
res.json({ message: 'User registered successfully' });
});
};

// Login a user and create a session
exports.loginUser = (req, res) => {

const { username, password } = req.body;

User.authenticate(username, password, (err, user) => {
if (err || !user) {
return res.status(401).json({ error: 'Invalid credentials' });
}

// Create a new session and store user id
req.session.userId = user._id;

res.json({ message: 'Login successful' });

});

};

Here is the index.js file

// Import required modules and files
const express = require('express');
const app = express();
const bodyParser = require('body-parser');
const authRoutes = require('./routes/auth');
const recordsRoutes = require('./routes/record');
const authController = require('./controllers/authController');
const recordsController = require('./controllers/recordsController');
const rbacMiddleware = require('./middleware/rbacMiddleware');

// Configure middleware
app.use(bodyParser.json());

// Define routes
app.use('/auth', authRoutes);
app.use('/records', rbacMiddleware.checkRole('user'), recordsRoutes);

// Start the server
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}`);
});

This index.js file sets up an Express.js server, imports the necessary modules, and configures the required middleware.

It also defines routes for authentication and tasks, utilizing the rbacMiddleware to check the user's role before allowing access to the records routes.

By implementing RBAC, you can significantly enhance the security of your Node.js applications, control user access effectively, and reduce the risk of security breaches.

--

--