Role Based Access Control in Node.js

Overview
In modern web applications, it’s crucial to control what different users can see and do. Role-Based Access Control (RBAC) is a system that assigns roles to users, such as admin, editor, or viewer, and restricts access to certain routes or features based on those roles. RBAC helps enforce security, maintain proper access privileges, and prevent unauthorized actions within your application. In this post, we will explore how to implement RBAC in Node.js, covering role assignment, route protection, middleware creation, and best practices for building secure and maintainable access control systems.


1. Introduction to Role-Based Access Control

RBAC is a security mechanism that determines access rights based on the roles assigned to users. Instead of managing permissions individually for each user, roles are created with predefined permissions, and users inherit the access rights associated with their role.

Benefits of RBAC:

  • Scalability: Easily manage access for a large number of users.
  • Maintainability: Centralized control over permissions simplifies updates.
  • Security: Reduces the risk of unauthorized access.
  • Flexibility: Supports different levels of access for various types of users.

Typical roles in a web application might include:

  • Admin: Full access to all resources and management capabilities.
  • Editor: Can modify content but has limited administrative access.
  • Viewer: Can only view content without making changes.

2. Setting Up a Node.js Application for RBAC

Before implementing RBAC, we need a basic Node.js application setup with user authentication. You can use Express.js and a database like MongoDB or MySQL.

Install dependencies using npm:

npm install express mongoose jsonwebtoken bcryptjs dotenv

2.1 Setting Up the Express Server

Create a basic Express server:

const express = require('express');
const app = express();
require('dotenv').config();

app.use(express.json());

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

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

3. Defining User Roles

User roles can be defined as constants or enums within your application. This ensures consistency when assigning and checking roles.

const ROLES = {
ADMIN: 'admin',
EDITOR: 'editor',
VIEWER: 'viewer'
}; module.exports = ROLES;

4. User Model with Role Field

When storing users in a database, include a role field to specify their access level. Using MongoDB with Mongoose as an example:

const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');

const userSchema = new mongoose.Schema({
username: { type: String, required: true, unique: true },
email: { type: String, required: true, unique: true },
password: { type: String, required: true },
role: { type: String, enum: ['admin', 'editor', 'viewer'], default: 'viewer' }
}); // Hash password before saving userSchema.pre('save', async function(next) {
if (!this.isModified('password')) return next();
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
}); module.exports = mongoose.model('User', userSchema);

Here, every user has a role that determines what they can access. The default role is viewer.


5. Authentication with JWT

RBAC works in conjunction with authentication. JSON Web Tokens (JWT) are commonly used to authenticate users and carry their role information.

5.1 User Login and Token Generation

const jwt = require('jsonwebtoken');
const User = require('./models/User');
const bcrypt = require('bcryptjs');

app.post('/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) return res.status(404).json({ message: 'User not found' });
const isMatch = await bcrypt.compare(password, user.password);
if (!isMatch) return res.status(401).json({ message: 'Invalid credentials' });
const token = jwt.sign(
    { userId: user._id, role: user.role },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
);
res.json({ token });
});

The token contains the user’s role, which will be used for access control.


6. Middleware for Role-Based Access Control

Middleware in Express allows you to check a user’s role before granting access to routes.

6.1 Authentication Middleware

First, create middleware to authenticate the JWT:

const jwt = require('jsonwebtoken');

function authenticate(req, res, next) {
const token = req.header('Authorization')?.replace('Bearer ', '');
if (!token) return res.status(401).json({ message: 'Access denied. No token provided.' });
try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
} catch (err) {
    res.status(400).json({ message: 'Invalid token' });
}
} module.exports = authenticate;

6.2 Authorization Middleware

Next, create middleware to check if the user has the required role(s) for a route:

function authorize(...allowedRoles) {
return (req, res, next) => {
    if (!req.user) return res.status(401).json({ message: 'User not authenticated' });
    if (!allowedRoles.includes(req.user.role)) {
        return res.status(403).json({ message: 'Access denied' });
    }
    next();
};
} module.exports = authorize;

This middleware allows you to specify which roles are permitted to access a particular route.


7. Protecting Routes with RBAC

Now that we have authentication and authorization middleware, we can protect routes according to user roles.

const authenticate = require('./middleware/authenticate');
const authorize = require('./middleware/authorize');
const ROLES = require('./roles');

app.get('/admin', authenticate, authorize(ROLES.ADMIN), (req, res) => {
res.send('Welcome Admin');
}); app.get('/edit', authenticate, authorize(ROLES.ADMIN, ROLES.EDITOR), (req, res) => {
res.send('Welcome Editor');
}); app.get('/view', authenticate, authorize(ROLES.ADMIN, ROLES.EDITOR, ROLES.VIEWER), (req, res) => {
res.send('Welcome Viewer');
});

Explanation:

  • /admin is accessible only by admins.
  • /edit is accessible by admins and editors.
  • /view is accessible by all roles.

8. Assigning and Changing Roles

Administrators should have the ability to assign roles to users. This can be done with an endpoint:

app.put('/users/:id/role', authenticate, authorize(ROLES.ADMIN), async (req, res) => {
const { role } = req.body;
const { id } = req.params;
if (!Object.values(ROLES).includes(role)) {
    return res.status(400).json({ message: 'Invalid role' });
}
try {
    const user = await User.findByIdAndUpdate(id, { role }, { new: true });
    if (!user) return res.status(404).json({ message: 'User not found' });
    res.json({ message: 'Role updated successfully', user });
} catch (err) {
    res.status(500).json({ message: 'Server error' });
}
});

9. Best Practices for Implementing RBAC

9.1 Principle of Least Privilege

Assign the minimal level of access necessary for users to perform their tasks. Avoid giving broad permissions unnecessarily.

9.2 Centralized Role Management

Keep roles and permissions centralized. Avoid hardcoding roles in multiple places in your codebase.

9.3 Dynamic Role Checking

Use middleware to dynamically check roles rather than embedding role checks in controllers. This promotes cleaner and maintainable code.

9.4 Logging and Auditing

Log attempts to access restricted routes. Auditing helps detect unauthorized access attempts and maintain security compliance.

9.5 Use Environment Variables

Store sensitive data such as JWT secrets in environment variables to maintain security and allow different configurations for development and production.


10. Advanced RBAC Concepts

10.1 Hierarchical Roles

Roles can be hierarchical, where higher roles inherit permissions from lower roles. For example, an admin automatically has editor and viewer permissions. This simplifies role management in larger applications.

10.2 Permission-Based Access Control

For complex applications, you might combine roles with permissions. Roles define groups of users, while permissions define specific actions (e.g., create_post, delete_post). Middleware then checks if the user’s role includes the necessary permission.

10.3 Integration with OAuth and SSO

RBAC can integrate with OAuth or Single Sign-On (SSO) providers. External authentication providers can provide role information, which can be used in Node.js to enforce access control.


11. Testing RBAC

Testing is crucial to ensure RBAC works as expected. Create test cases for:

  • Users with different roles accessing protected routes
  • Users attempting actions beyond their permissions
  • Role assignment and updates
  • Token validation and expiration

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *