Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to…

Follow publication

Implementing Role-Based Access Control using CASL

--

Hey everyone, I’m back to blogging after a year I guess! I have started Working in a software company managing security and handling MERN stack projects, which has kept me busy. But I managed to get back to give you guys a treat! Today, I want to share how I’ve implemented Role-Based Access Control (RBAC) to enhance the security of my projects.

OVERVIEW

Let’s take a mobile or web application as a example where the tasks you can perform depend on the role you have. For instance, as a regular user, you might only be able to view and update your own profile information. Meanwhile, an administrator would have broader access, allowing them to manage user accounts, adjust application settings, and oversee the system’s overall functionality.

This concept is known as Role-Based Access Control (RBAC) in web applications. RBAC ensures that each user is restricted to actions permitted by their assigned role. This structured approach not only maintains orderliness within the application but also enhances security by limiting access to sensitive functionalities and data.

In general, RBAC provides a clear framework where users are granted appropriate permissions based on their roles, thereby fostering a secure and organized user experience.

Understanding RBAC: Roles and Permissions

RBAC works by assigning users to specific roles (e.g., user, admin, superadmin). Each role is granted a set of permissions (e.g., manage, read, create, delete, update) that define a user’s allowed actions within the application. This separation of roles and permissions ensures a secure and organized access control system.

introducing CASL: Your Authorization Castle

CASL acts as your central hub for managing user access in a JavaScript application. It provides a structured approach to define roles, permissions, and user abilities. With CASL, you can:

  • Define Roles and Permissions: Create roles like “user” and “admin,” and assign granular permissions (manage, read, update, create, delete) to each role.
  • Craft User Abilities: Based on a user’s role, CASL generates an “Ability” object that encapsulates their allowed actions on specific resources.
  • Enforce Authorization: Integrate CASL into your application’s logic to check user abilities before granting access to resources or functionalities.

Building Your Secure Realm with CASL (Code Breakdown)

I have written a code as how it needed to be, let’s dissect it to get an understanding ;)

1. Defining Actions, Subjects, and User Roles

First, we define the actions and subjects relevant to our application:

import { AbilityBuilder, AbilityClass, ExtractSubjectType, PureAbility } from '@casl/ability';

// Define the actions that can be performed
export type Actions = 'manage' | 'read' | 'create' | 'delete' | 'update';

// Define the subjects that these actions can be performed on
export type Subjects = 'bookings' | 'membership' | 'users' | 'all';

// Define a custom ability type that uses our actions and subjects
export type AppAbility = PureAbility<[Actions, Subjects]>;

// Define a type for role permissions that maps actions to subjects
type RolePermissions = {
[key in Actions]?: Subjects[];
};

// Define the permissions for each role in our application
type Permissions = {
[key: string]: RolePermissions;
};

// Predefine permissions for each role
const predefinedPermissions: Permissions = {
USER: {
manage: ['bookings'],
read: ['membership', 'users'],
create: ['membership]
update: ['membership'],
},
ADMIN: {
manage: ['membership'],
delete: ['bookings']
read: ['users'],
},
SUPERADMIN: {
manage: ['users'],
},
};

// Function to define abilities for a given role
export function defineAbilitiesFor(role: string) {
const { can, build } = new AbilityBuilder<AppAbility>(PureAbility as AbilityClass<AppAbility>);

// Get the permissions for the given role
const permissions = predefinedPermissions[role] || {};

// Define the abilities based on the permissions
Object.keys(permissions).forEach((action) => {
(permissions[action as Actions] as Subjects[]).forEach((subject) => {
can(action as Actions, subject as ExtractSubjectType<Subjects>);
});
});

// Build and return the abilities
return build();
}

This code defines the actions and subjects, creates a custom AppAbility type, and sets up predefined permissions for different roles. The defineAbilitiesFor function uses these predefined permissions to create an Ability object for a given role.

2. User Authentication and Role Extraction

Next, we validate the user’s JWT token and extract their role:

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';

// Extend the Express Request interface to include decodedToken
interface AuthenticatedRequest extends Request {
decodedToken?: { userId: number, role: string };
}

// Middleware to validate JWT token
const validateToken = (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
try {
// Get the authorization header from the request
const authorizationHeader = req.headers['authorization'];
if (!authorizationHeader) {
throw new Error("Authorization header missing");
}

// Split the authorization header into its components
const authHeaderParts = authorizationHeader.split(' ');
if (authHeaderParts.length !== 2) {
throw new Error("Invalid Authorization header format");
}
const authTokenFromHeader = authHeaderParts[1];

// Verify the JWT token
const secretKey = process.env.JWT_SECRET || 'your JWT secret';
const decodedToken = jwt.verify(authTokenFromHeader, secretKey) as { userId: number, role: string };
req.decodedToken = decodedToken;

// Proceed to the next middleware
next();
} catch (error: unknown) {
const errorMessage = typeof error === 'string' ? error : (error as Error).message || 'An error occurred';
console.error('Authentication error:', errorMessage);
res.status(401).json({ success: false, message: errorMessage });
}
};

export { validateToken };

The validateToken middleware intercepts user requests, verifies JWT tokens, and extracts relevant information like user ID and role from the decoded token. This data is stored in the extended req.decodedToken property.

I hope you guys know how to implement a JWT creation in the login functionality and make sure to add a role in the token with the existing things added.

3. Ability Definition based on Role

The defineAbilitiesFor function creates an Ability object based on the predefined permissions for a given role:

export function defineAbilitiesFor(role: string) {
const { can, build } = new AbilityBuilder<AppAbility>(PureAbility as AbilityClass<AppAbility>);

const permissions = predefinedPermissions[role] || {};

Object.keys(permissions).forEach((action) => {
(permissions[action as Actions] as Subjects[]).forEach((subject) => {
can(action as Actions, subject as ExtractSubjectType<Subjects>);
});
});

return build();
}

4. Authorization Middleware with CASL Checks

We then use the defineAbilitiesFor function within the authorizeUser middleware to check if the user has the necessary permissions:

import { defineAbilitiesFor, Actions, Subjects } from '../util/abilities';
import { ForbiddenError } from '@casl/ability';

const authorizeUser = (action: Actions, subject: Subjects) => (req: AuthenticatedRequest, res: Response, next: NextFunction): void => {
try {
const decodedToken = req.decodedToken;
if (!decodedToken || !decodedToken.role) {
throw new Error("Invalid token or role not found");
}

// Define the abilities for the user's role
const abilities = defineAbilitiesFor(decodedToken.role);
// Check if the user has the required permission
ForbiddenError.from(abilities).throwUnlessCan(action, subject);

// Proceed to the next middleware
next();
} catch (error: unknown) {
const errorMessage = typeof error === 'string' ? error : (error as Error).message || 'An error occurred';
console.error('Authorization error:', errorMessage);
res.status(403).json({ success: false, message: errorMessage });
}
};

export { authorizeUser };

The authorizeUser middleware retrieves the user’s role from the decoded token and builds their abilities using defineAbilitiesFor. It then uses CASL’s ForbiddenError.from to check if the user’s abilities permit the requested action on the specified subject. If unauthorized, a ForbiddenError is thrown, resulting in a 403 response.

5. Implementing in Routes

Finally, we integrate these middlewares into our routes to enforce authentication and authorization:

import express from 'express';
import { validateToken, authorizeUser } from './middlewares/authMiddleware';
import UserController from './controllers/UserController';

const router = express.Router();

router.get(
"/", validateToken, authorizeUser('read', 'users'),
UserController.get
);

router.delete(
"/:id", validateToken, authorizeUser('manage', 'users'),
UserController.delete
);

export default router;

Now we would get the desired results we required for the RBAC!

Benefits of CASL-powered RBAC

Using CASL for RBAC implementation offers several advantages:

  • Simplified Management: CASL centralizes role and permission definitions, making them easier to manage and maintain.
  • Granular Control: CASL allows you to define fine-grained permissions, ensuring that users have exactly the access they need.
  • Enhanced Security: By enforcing permissions at the application level, CASL helps prevent unauthorized access and enhances overall security.

With CASL, you can build a robust and secure RBAC system that scales with your application’s needs. Whether you’re managing a small application or a large enterprise application, CASL provides the tools you need to control access effectively.

By following this structured approach, you can ensure that each user in your application has access only to the resources they need, enhancing both security and usability.

Also, check out CASL site for docs!

I will also provide my GitHub link for this repo, where I will have two files 1 would be authMiddleware.ts and abilities.ts!

I hope I have given you guys a starter plate for implementing the role-based access control, Do DM me on any of the social media below so that I can help you out if there are any issues!

https://www.instagram.com/fazalur.png

https://linkedin.com/in/fazalur-rahman-43637b1b3

Stackademic 🎓

Thank you for reading until the end. Before you go:

--

--

Published in Stackademic

Stackademic is a learning hub for programmers, devs, coders, and engineers. Our goal is to democratize free coding education for the world.

Written by Fazal

Security Analyst | Bug Hunter | google VRP researcher | Developer

No responses yet

Write a response