Published on

Implementing Role-Based Access Control (RBAC) in Node.js/Express with TypeScript and MongoDB

Authors
Buy Me A Coffee

Table of Contents

Introduction

In the realm of web development, security is paramount. One fundamental aspect of securing web applications is controlling access to resources based on user roles. Role-Based Access Control (RBAC) provides a structured approach to managing user privileges and permissions within an application.

What is Role-Based Access Control (RBAC)?

RBAC is a method of restricting system access to authorized users based on their roles within an organization. It simplifies access management by assigning specific roles to users and granting permissions based on those roles.

Why Implement RBAC?

  1. Granular access control: Administrators can define fine-grained access permissions for different user roles.
  2. scalability: As applications grow, RBAC scales efficiently by managing access control through role assignments.
  3. Ehanced Security: RBAC helps prevent unauthorized access and minimizes the risk of data breaches.

IMPORTANT NOTE:

I will be integrating RBAC in the existing Node.js/Express and MongoDB REST API application implemented in the previous article. You can also find the source code on GitHub. Remember to checkout the branch ft-implementing-rbac for the RBAC implementation.


Steps to integrate Role-Based Access Control in our project

  1. Defining User Roles

Identify the roles that users can have in the system, such as "admin", "user", "moderator", etc. For purposes of this blog, we will define 2 roles - user and admin.

Create a file called roles.ts in the /middleware directory and in there, define a UserRole enum.

export enum UserRole {
  Admin = 'admin',
  User = 'user',
}
  1. Update User schema

Navigate to /models/user.tsx and add a role field to the user schema to store the user's role. Our user schema now looks like this:

import { UserRole } from "../middleware/roles";

export interface IUser extends Document {
  name: string;
  email: string;
  password: string;
  tokens: {token: string }[];
  role: UserRole // role is added to the IUser interface
}

const userSchema = new Schema<IUser, UserModel, IUserMethods>({
  name: { type: String, required: true },
  email: { type: String, required: true },
  password: { type: String, required: true },
  tokens: [{ token: { type: String, required: true } }],
  role: { type: String, required: true, default: UserRole.User} // Added a role field
});

In the code above, we added the role field on the IUser interface and on the schema object. As you can see, we have set the default role to be user. That means that everytime a new user is created, a user role will be automatically assigned.

  1. Create authorization middleware to handle user roles

Navigate to the roles middleware in /middleware/roles.ts and add the following code.

import { CustomRequest } from "./auth";

// Middleware to check if user is admin
export const isAdmin = (
  req: CustomRequest,
  res: Response,
  next: NextFunction
) => {
  if (req.user && req.user.role === UserRole.Admin) {
    return next(); // User is admin, allow access
  } else {
    return res.status(403).json({ message: "Unauthorized" }); // User is not admin, deny access
  }
};

In the code above, we've created a middleware isAdmin that can be hooked into any route to check if the person trying to access that resource is and admin or not. If they are not, a 403 Unauthorized error will be thrown. This middleware is supposed to be used in conjuction with the auth middleware which we created here.

  1. Protect Routes Once our middleware is in place, we can now use it to protect routes. Before we create the route to fetch all users, let's first create a controller for it. Head over to /controllers/userController.ts and add this code.
export const fetchUsers = async () => {
  const users = await User.find({});
  return users;
}

The code above simply fetches all users from our database.

Let's now create an admin only route in userRoutes.ts file to fetch all users.

import { isAdmin } from "../middleware/roles";
import {fetchUsers} from "../controllers/userController";

// Fetch all users. This route is only accessible by the admin.
router.get("/admin", auth, isAdmin, async (req, res) => {
  const allUsers = await fetchUsers();
  return res.status(200).json(allUsers);
});
  1. Test the protected route with Postman

Spin up Postman. If you don't know how to then I suggest you follow these steps to download and install Postman. Your route should be a GET request to localhost:3000/api/users/admin. You can try to test it out for different cases.

  • Test with no authentication token: You should receive a 401 Unauthorized with the following error message:
{
   "error": "Authentication failed."
}
  • Test with authentication but with a normal user role: You will have to login with a user account and get the jwt token and then pass it in the Authorization header. Since the api/users/admin endpoint can only be accessed by admin roles. You should get a 403 Forbidden with the following error:
{
   "message": "Unauthorized"
}
  • Test with an admin role. You can directly change the role of one of your users from the Atlas database to admin for testing purposes. Login with that users credentials and get that authentication token to pass along in our /api/users/admin endpoint. Once you send that request, you should be able to access the list of users.

Conclusion

Implementing RBAC in your application enhances security and ensures that users have appropriate access to resources. By following best practices and integrating RBAC effectively, you can create a robust and secure web application that meets the needs of your users and protects sensitive data.

Next Steps

Remember this is a basic implementation of RBAC. You can expand on it by creating a seperate model for roles and by giving permissions to each role. Therefore, experiment with RBAC in your own projects, explore additional features such as role hierarchy and dynamic permissions, and stay updated on emerging security trends and best practices in access control.

Code Repo

You can find all the code on this Github Repo. Rememeber to checkout to the branch ft-implementing-rbac.

Till next time. Happy Coding!👨🏾‍💻