DEV Community

oyedeletemitope
oyedeletemitope

Posted on

Beginners Guide to Managing User Roles and Pemissions in Node.js

As your application grows, managing user roles becomes increasingly important for ensuring proper access control and security. In this article, we'll look at managing user roles and permissions in your Node.js application.

Understanding User Roles

Before diving into the technical implementation, it's important to understand user roles and why they are necessary. A user role defines a set of permissions or privileges that a user has within an application. For example, in a blogging platform, a user with the " author " role may be able to create and edit their posts but not publish them.

A user with the role of "editor" may be able to create, edit, and publish posts but not delete them. A user with the " admin " role may have complete control over all aspects of the application, including managing users and roles.

Role-Based Access Control (RBAC)

Role-Based Access Control (RBAC), also known as role-based security, is a method of access control that assigns permissions to users based on their role within an organization. It provides a manageable, less error-prone approach to access management than individually assigning permissions, ensuring that users can only access information and perform actions necessary for their jobs. This concept is known as the principle of least privilege.

How RBAC Works

RBAC works by associating roles with specific permissions and then assigning these roles to users. A role represents a job function within an organization, and each role is associated with a set of permissions that define the operations that can be performed by a user assigned to the role. For instance, you might have roles such as "administrator," "manager," or "employee," each with different permissions. When a user is assigned a role, they gain the permissions associated with it.

Implementing User Roles in Node.js

Let's see how we can implement and manage user roles with an example. To get started, I'll provide a basic boilerplate code, which you can get on Github to follow through this tutorial. So, I'll be explaining the files:

  • Data.js: This will be taking the place of a database. It contains all the different permission roles in our system. The same thing goes for tasks, with each user having their own tasks.

  • Server.js: here, we are just starting up an express server and importing all the required dependencies and files that we need, telling it to use JSON so we can pull in data from a post request. It also consists of a few basic routes, i.e., the get routes for the homage, dashboard routes, etc. Finally, we have middleware, which sets our user.
    routes/tasks.js: contains routes, i.e., get routes to get all the tasks, a single task, and another route to task for our get route.

Once we've cloned the repo, run:

npm i 
Enter fullscreen mode Exit fullscreen mode

This is to install the necessary dependencies. We also want to install nodemon too . Run:

npm install nodemon
Enter fullscreen mode Exit fullscreen mode

and then start up the development server by running this command below:

npm run dev
Enter fullscreen mode Exit fullscreen mode

To make an API request, I'll be using Rest Client in vs. code. You could use Postman, Thuderclient, or whichever one you prefer. To use the rest client, install from the VsCode extension market. Once installed, create a file called test.rest for testing out our routes.

Now, we have a system with different routes, but none of these routes are protected. For example, anyone could access the homepage, but the admin page is also accessible. If we send in a request, we can see that even though we are not logged in, we still have access to the admin page.

We need to make sure that we lock down these different pages based on the role, the tasks they have access to, and whether or not the user is logged in.

Before we begin, let's import everything we'll need inside our server.js. So we won't have to go back again to import them every time we want to work on our server.js during the tutorial process:

const { ROLE, users } = require("./data");
const { authUser, authRole } = require("./basicAuth");
Enter fullscreen mode Exit fullscreen mode

Now, we can proceed with the tutorial.

So, let's start with the most basic form of authentication, and that is whether or not the user is logged in. We want to write a form of middleware that will take in the user, check if the user exists, continue, or otherwise send an error message.

Inside the routes folder, create a file called basicAuth.js. This will be for our basic authentication. Create a function called authUser() like so:

function authUser(req, res, next) {
 if (req.user == null) {
   res.status(403);
   return res.send("you need to sign in ");
 }
 next();
}
Enter fullscreen mode Exit fullscreen mode

We create a function called authUser(), a middleware that takes in a request, response, and next. This will check if the user exists and if they are logged in, and if they are, continue; otherwise, it will be an error. Head over to server.js and add the authUser to our dashboard routes. Like so:

app.get("/dashboard", authUser, (req, res) => {
 res.send("Dashboard Page");
});
Enter fullscreen mode Exit fullscreen mode

Testing it out by sending a request. If we send a request with the userId of 1, you'll notice we get a response taking us to the dashboard:

dashboard authentication

If we don't have a user, it'll tell us to sign in:

user has to sign in to access the dashboard

So, we now have a basic form of authentication, i.e only logged-in users can access the dashboard, leaving only the homepage accessible to all.

The next form of authentication is for our admin page because we only want our admins to have access because right now, every user and non-logged-in user can access it. The first thing we need to do is check if they are a user and then if they are an admin for them to access the admin page. Inside our server.js, add the authUser to our admin route, making it look like so:

app.get("/admin", authUser,(req, res) => {
 res.send("Admin Page");
});
module.exports = { authUser };
Enter fullscreen mode Exit fullscreen mode

To authenticate this to a specific role, we need to write some codes in our basicAuth to achieve that. So, inside our basicAuth.js, create a function that takes in the role that we want to authenticate, and inside that function, we need to return a middleware, which is the req, res, and next:

function authRole(role) {
 return (req, res, next) => {
   if (req.user.role !== role) {
     res.status(401);
     return res.send("not allowed");
   }
   next();
 };
Enter fullscreen mode Exit fullscreen mode

In the code, we have a middleware function that checks the role of the user making a request. The middleware takes three parameters: req (request), res (response), and next (a function that passes control to the next middleware or route handler).

If the user's role does not match the specified role, the middleware sets the HTTP status code to 401 (Unauthorized) and returns a response stating "Not allowed." This restricts access to certain routes or actions based on user roles. Let's not forget to export the Authrole too:

module.exports = { authUser, authRole };
Enter fullscreen mode Exit fullscreen mode

Remember, we already imported everything we'll need in our server.js before the start of this tutorial. So, in our server.js, add the function to the admin route like so:

app.get("/admin", authUser, authRole(ROLE.ADMIN), (req, res) => {
 res.send("Admin Page");
});
Enter fullscreen mode Exit fullscreen mode

So first, we are authenticating a user and then authenticating that a user has the role of admin, and if both are true, then we run the routes that are accessible. So, let's test our admin route:

For user 1:

admin having only the access to the database

For user 2 or 3:

user 2 not authenticated

You'll notice that only admins can now access the route. Now, this should cover most of the authentication needs, but we need to be a little more specific when it comes to specific routes, such as task routes.

Recall that in our data, each user has a specific task that they are linked to, except the admin, who can access every user's task. So we need to set up that proper authentication so as not to let any user have access to tasks not linked.

Create a new folder called permissions. This is where we'll create permissions for our specific routes. Inside the folder, create a new file called tasks.js. First, we'll create a function that checkers if a user can have access to a task:

const { ROLE } = require("../data");
function canViewTask(user, task) {
 return user.role === ROLE.ADMIN || task.userId == user.id;
}
module.exports = {
 canViewTask,
};
Enter fullscreen mode Exit fullscreen mode

In the code above, the canViewTask function checks whether a given user has the authority to view a specific task. It returns true if the user's role is "ADMIN" or if the task's user matches the user's ID ( if a user is part of the tasks). In simple terms, only an admin (ROLE.ADMIN) can view any task, while a regular user can only view their own tasks.

Inside our tasks.js, We need to set up authentication around it. We first want to authenticate that the user exists; we only want people to access this page if they are signed in. Let's import the userAuth function into our task.js:

const { authUser } = require("../basicAuth");
Enter fullscreen mode Exit fullscreen mode

Then we add it to the task route:

router.get("/:taskId", setTask, authUser, authGetTask, (req, res) => {
 res.json(req.task);
});
Enter fullscreen mode Exit fullscreen mode

Notice the authGetTask? Don't worry; we'll create that shortly. Then, we want to bring in our authentication for our different permissions for our canviewTask:

const {canViewTask,} = require("../permissions/taskPermission");
Let's now create the authGetTask function:
function authGetTask(req, res, next) {
 if (!canViewTask(req.user, req.task)) {
   res.status(401);
   return res.send("not allowed ");
 }
 next();
}
Enter fullscreen mode Exit fullscreen mode

The code above is a middleware that checks if a user is authorized to view a specific task by invoking the canViewTask function. If the user is not authorized, it sends a "not allowed" response with a status code of 401. Otherwise, the request can proceed to the next middleware or route handler.

Let's test it out to see if the authentication works.

You'll see that user 3 shows "not allowed" when we use it to access a task that's not linked to it:

user 3 is not allowed to view the task

user 2 and user 1 can access it because user 2 is linked to it and user 1 is the admin:

user 2 can view the task as it is linked to it

user 1 can view the task as it is the admin

The next thing we want to work on is the task route. This is going to return all of the tasks, but now, all users can see all the tasks displayed at once, which should not be so.

The admin should only be the one allowed to see the full list of the tasks, while the user should only get to view their task list alone, so we need to create yet another function inside our task permissions.js to handle which actual task the user has access to view.

So, let's go ahead and create a function called scopedProjects like so:

function scopedTasks(user, tasks) {
 if (user.role === ROLE.ADMIN) return tasks;
 return tasks.filter((task) => task.userId === user.id);
}
Enter fullscreen mode Exit fullscreen mode

The code above filters and returns a subset of tasks based on the user's role. It is an example of a role-based access control system that checks if the user's role equals the "ADMIN" role. If the user is an administrator, the function returns the original array of tasks (tasks) without filtering.

If the user is not an administrator, it uses the filter method to create a new array containing only the tasks where the userId property matches the user ID.

Our module.exports now looks like this:

module.exports = {
 canViewTask,
 scopedTasks,
};
Enter fullscreen mode Exit fullscreen mode

Inside task.js, let's change our first routes to this:

router.get("/", authUser, (req, res) => {
 res.json(scopedTasks(req.user, tasks));
});
Enter fullscreen mode Exit fullscreen mode

In the code above, we first ensure that only authenticated users can access the subsequent route. Then we pass a list of all the tasks as well as the particular user, scoping the list of projects down to that individual user. Let's test it out:

user 1 can view all the tasks

user 2 can view only tasks linked to it

user 3 can view onl tasks linked to it

We can see that the admin, which is user 1, displays all the tasks, user 2 displays the tasks it is linked to, and likewise, user 3. Now, this system is handling the list and permission for our routes.

And that's a wrap, guys! You can add another route for deleting and creating a task if you want.

Conclusion

Managing user roles in Node.js is crucial for maintaining a secure and organized web application. By implementing role-based access control, integrating with a database, and using middleware for authentication and authorization, you can establish a robust system that protects sensitive data and ensures a smooth user experience.

Stay informed about best practices and security updates to keep your application secure and up-to-date.

Top comments (1)

Collapse
 
deniskiplangat profile image
denis kiplangat

Nice content. Might need some improvement but still nice.