Node.js

Role-Based Access Control in Node.js with JWT

Role-Based Access Control (RBAC) is a fundamental security pattern used to restrict system access based on user roles. When combined with JSON Web Tokens (JWT), it enables stateless, scalable, and secure authorization in modern APIs. In this article, we will walk through how to implement RBAC in a Node.js REST API using JWT.

1. RBAC and JWT Overview

Role based access control (RBAC) is an authorization model that restricts system access based on predefined roles assigned to users. Instead of assigning permissions directly to individual users, permissions are grouped into roles, and users inherit those permissions through their assigned roles. This approach simplifies access management, improves consistency, and enhances security by ensuring that users perform only actions aligned with their responsibilities.

JSON Web Tokens (JWT) are a compact, secure way to transmit information between parties as digitally signed tokens. In the context of APIs, JWTs are commonly used for stateless authentication. Once a user is authenticated, a token is issued containing encoded claims such as user identity and role. This token is included in subsequent requests, allowing the server to verify the user and enforce RBAC rules without maintaining session state.

When combined, RBAC and JWT provide a scalable and efficient mechanism for securing REST APIs. JWT handles authentication by verifying identity, while RBAC governs authorization by determining what actions the authenticated user is permitted to perform.

2. Prerequisites

A foundational understanding of Node.js and Express is required to follow this article effectively. Familiarity with REST API, JavaScript fundamentals, and basic authentication concepts will also be beneficial.

Ensure that Node.js (version 18 or higher) is installed and properly configured on your system. Additionally, you should be comfortable using a terminal or command-line interface to run and manage Node.js applications.

The implementation in this article utilizes the following libraries:

3. Project Setup

First, we will initialize a Node.js project and install the dependencies required to build our authentication and authorization system.

mkdir role-based-auth-api
cd role-based-auth-api
npm init -y

npm install express jsonwebtoken bcryptjs dotenv

Create a basic server file (src/index.js):

const express = require('express');
const app = express();
app.use(express.json());
app.get('/', (req, res) => {
  res.json({ message: 'RBAC API Guide' });
});
app.listen(3000, () => {
    console.log('Server running on port 3000');
});

This sets up a simple Express server. We install the following libraries:

  • express for routing
  • jsonwebtoken for token creation and verification
  • bcryptjs for password hashing
  • dotenv for managing environment variables securely
Tip
After installing the main libraries, install nodemon as a development dependency using npm install --save-dev nodemon. This tool automatically restarts the server whenever file changes are detected, improving development efficiency.

Environment Configuration

Environment variables allow sensitive configuration to be managed securely outside the codebase. Create a .env file and add the following:

SECRET_KEY=my_secure_secret

The SECRET_KEY is used to sign JWT tokens. Using environment variables ensures flexibility across different environments such as development and production.

4. Setting Up the Data Store

To keep things simple, we will use an in-memory data store to simulate a database. This will hold users and their roles.

src/data/store.js

const bcrypt = require('bcryptjs'); 
// Pre-hashed password for "password123" 
const hashedPassword = bcrypt.hashSync('password123', 8);

const users = [
    {id: 1, username: 'admin', password: hashedPassword, roleId: 1}, 
    {id: 2, username: 'thomas', password: hashedPassword, roleId: 2}
];

const roles = [
    {id: 1, name: 'admin'},
    {id: 2, name: 'user'}
];

module.exports = {users, roles};



This sample dataset provides two predefined roles—admin and user—and two corresponding users for testing. Both users share the same password (password123), which is securely hashed using bcrypt. The roleId field links each user to a role, enabling immediate testing of authentication and role-based authorization flows.

Protected Resource

src/data/projects.js

const projects = [
    {
        id: 1,
        name: "Website Redesign",
        description: "Revamp the corporate website UI/UX",
        owner: 1
    },
    {
        id: 2,
        name: "Mobile App Development",
        description: "Build a cross-platform mobile application",
        owner: 2
    }
];

module.exports = {projects};

This sample dataset provides initial project records to simulate real-world usage. Each project includes an id, name, description, and an owner linked to a user ID, allowing us to test both access control and data retrieval immediately. Users are assigned roles, and projects act as protected resources that require authorization.

5. Building the Auth Routes

In this section, we will implement the core authentication logic, including user registration and login. These operations will allow users to securely create accounts and obtain JSON Web Tokens (JWTs) for subsequent requests.

To achieve this, we will create an authentication controller that handles the business logic and an authentication routes file that maps HTTP endpoints to the controller functions.

Authentication Controller

The controller below is responsible for handling incoming requests, validating user data, securely storing credentials, and generating tokens.

src/auth/authController.js

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const {users, roles} = require('../data/store');

const SECRET = process.env.SECRET_KEY;

exports.register = async (req, res) => {
    const {username, password, roleId} = req.body;

    const role = roles.find(r => r.id === roleId);
    if (!role)
        return res.status(400).json({message: 'Invalid role'});

    const hashedPassword = await bcrypt.hash(password, 8);

    const user = {
        id: users.length + 1,
        username,
        password: hashedPassword,
        roleId
    };

    users.push(user);
    res.status(201).json({message: 'User registered'});
};

exports.login = async (req, res) => {
    const {username, password} = req.body;

    const user = users.find(u => u.username === username);
    if (!user)
        return res.status(404).json({message: 'User not found'});

    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid)
        return res.status(401).json({message: 'Invalid credentials'});

    const token = jwt.sign(
            {id: user.id, roleId: user.roleId},
            SECRET,
            {expiresIn: '1h'});

    res.json({token});
};

The register function handles new user creation while the login function authenticates existing users.

Authentication Routes

With the controller in place, we now define the routes that expose these authentication functionalities to clients.

src/auth/authRoutes.js

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

router.post('/register', authController.register); 
router.post('/login', authController.login); 

module.exports = router;

These routes map HTTP POST requests to the corresponding controller methods. The /register endpoint allows new users to create accounts, while the /login endpoint authenticates users and returns a JWT token.

6. Implementing Authentication and Role-Based Access Control Middleware

To secure the API, we need middleware that verifies user identities and enforces role-based access control. This involves two components: verifying JWT tokens to authenticate users and restricting access to specific routes based on user roles.

JWT Verification Middleware

Before allowing access to protected routes, we must confirm that the request includes a valid JSON Web Token (JWT). This middleware extracts the token from the request headers and verifies it using a secret key.

src/middleware/authMiddleware.js

const jwt = require('jsonwebtoken');
const SECRET = process.env.SECRET_KEY;

module.exports = function (req, res, next) {
    const header = req.headers['authorization'];
    if (!header) {
        return res.status(403).json({message: 'No token provided'});
    }

    const token = header.split(' ')[1];
    jwt.verify(token, SECRET, (err, decoded) => {
        if (err)
            return res.status(401).json({message: 'Invalid token'});
        req.user = decoded;
        next();
    });
};

Role-Based Authorization Middleware

Once a user is authenticated, we need to control what they are allowed to do. This middleware enforces role-based access control (RBAC) by checking whether the user’s role is permitted to access a specific route.

src/middleware/roleMiddleware.js

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

This middleware is designed as a higher-order function that accepts an array of allowed role IDs. It returns a function that checks whether the authenticated user’s roleId exists in that array.

If the user’s role is not included, the request is denied with a 403 Forbidden response. Otherwise, the request proceeds to the next route handler. This design allows us to easily protect routes by specifying which roles are allowed.

7. Building the Protected Routes

With authentication and authorization in place, we can now build protected routes that allow users to interact with application resources. These routes will demonstrate how authenticated users can retrieve data, while only authorized roles can create new resources

Controller

The controller below contains the logic for handling operations such as retrieving all projects and creating new ones.

src/projects/projectController.js

const {projects} = require('../data/projects');

exports.getProjects = (req, res) => {
    res.json(projects);
};

exports.createProject = (req, res) => {
    const project = {
        id: projects.length + 1,
        name: req.body.name,
        owner: req.user.id
    };

    projects.push(project);
    res.status(201).json(project);
};

Routes

Next, we define the routes that expose these controller functions and apply the necessary security middleware.

src/projects/projectRoutes.js

const express = require('express');
const router = express.Router();
const authMiddleware = require('../middleware/authMiddleware');
const roleMiddleware = require('../middleware/roleMiddleware');
const controller = require('./projectController');

router.get('/', authMiddleware, controller.getProjects);
router.post('/', authMiddleware, roleMiddleware([1]), controller.createProject);

module.exports = router;

The GET / route is protected using the authentication middleware, meaning only users with a valid JWT token can retrieve projects.

The POST / route goes a step further by applying both authentication and role-based authorization. While all users must be authenticated, only users with a role ID of 1 (an admin role) are allowed to create new projects. If a user without the required role attempts this action, the request will be denied.

Putting It All Together

The main application file acts as the entry point. It initializes the Express server, applies global middleware, and mounts the different route modules under appropriate URL prefixes.

index.js

const express = require('express');
require('dotenv').config();
const authRoutes = require('./auth/authRoutes');
const projectRoutes = require('./projects/projectRoutes');

const app = express();

app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/projects', projectRoutes);

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


Requests to /api/auth are routed to the authentication logic, while requests to /api/projects are handled by the project routes.

8. Testing the API

The final step is to test the API endpoints to ensure everything works as expected.

update package.json (scripts section)

"scripts": 
{ 
  "start": "node server.js", 
  "dev": "nodemon server.js" 
}

You need to start the server using your npm script.

npm run dev

Register a User

To begin, we create a new user account by sending a POST request to the registration endpoint.

curl -X POST http://localhost:3000/api/auth/register \
-H "Content-Type: application/json" \
-d '{
  "username": "thomas",
  "password": "password123",
  "roleId": 2
}'

After running this command, you should receive a response confirming that the user has been successfully created:

{ "message": "User registered" }

At this point, the user thomas exists in the system with a role ID of 2 (a standard user role). This account will be used in the next steps.

Login

Next, we authenticate the user by sending their credentials to the login endpoint. This step verifies the username and password, and returns a JWT token if successful.

curl -X POST http://localhost:3000/api/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "thomas",
    "password": "password123"
  }'

If the credentials are valid, the response will include a token:

{ "token": "your_jwt_token" }

Copy the token from the response. This token is essential for accessing protected routes, as it represents the authenticated user.

Access Protected Route

With a valid token, you can now access protected endpoints such as retrieving all projects. This request includes the token in the Authorization header.

curl -X GET http://localhost:3000/api/projects \
  -H "Authorization: Bearer <token>"

Replace <token> with the actual JWT obtained from the login step. If the token is valid, the server will respond with the list of projects.

[
 {
   "id":1,
   "name":"Website Redesign",
   "description":"Revamp the corporate website UI/UX",
   "owner":1
 },
 {
   "id":2,
   "name":"Mobile App Development",
   "description":"Build a cross-platform mobile application",
   "owner":2
 }
]

This confirms that authentication is working correctly and that only authorized users can access the resource.

Test Role Restriction

Finally, we test role-based access control by attempting to create a new project. This route is restricted to users with specific roles (e.g., admin role with ID 1).

curl -X POST http://localhost:3000/api/projects \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Complete Exercise",
    "description": "Go for a 5000 meters run"
  }'

If the user does not have the required role, the response will be:

{ "message": "Access denied" }

This demonstrates that role-based authorization is functioning correctly. Only users with the appropriate permissions can perform restricted actions such as creating new projects.

To test a successful scenario, log in with a user that has an admin role (roleId: 1) and repeat the request. You should then receive a successful response.

9. Conclusion

In this article, we implemented an RBAC system in a Node.js REST API using JWT. We covered user authentication, role management, middleware design, and route protection. While we used an in-memory store for simplicity, this pattern can easily be extended to real databases and production systems. JWT provides stateless authentication, while RBAC ensures users can only access resources permitted by their roles.

10. Download the Source Code

Download
You can download the full source code of this example here: role based access control nodejs rest api jwt

Omozegie Aziegbe

Omos Aziegbe is a technical writer and web/application developer with a BSc in Computer Science and Software Engineering from the University of Bedfordshire. Specializing in Java enterprise applications with the Jakarta EE framework, Omos also works with HTML5, CSS, and JavaScript for web development. As a freelance web developer, Omos combines technical expertise with research and writing on topics such as software engineering, programming, web application development, computer science, and technology.
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button