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.
RBAC ensures 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
How RBAC works is quite straightforward. It associates roles with specific permissions and then assigns these roles to users.
For example, let's say you have roles such as administrator,
manager,
or employee
in your application, each with different permissions. When a user is assigned any of these roles, they get the permissions that come with it.
How to Implement 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
This is to install the necessary dependencies. We also want to install nodemon too. Run this command:
npm install nodemon
Afterwards, start up the development server by running this command below:
npm run dev
To make an API request, I'll be using Rest Client in VsCode. 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, you have a system with different routes, but none of these routes are protected, i.e., anyone could access the homepage, and the admin page is also accessible. If you send in a request, you can see that even though you are not logged in, you still have access to the admin page.
You need to make sure that you 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 you won't have to go back again to import them every time you want to work on server.js
during the tutorial process:
const { ROLE, users } = require("./data");
const { authUser, authRole } = require("./basicAuth");
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. You need 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 the 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();
}
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. If they do exist, then 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");
});
Testing it out by sending a request. If you send a request with the userId
of 1, you'll notice you get a response taking you to the dashboard:
If there's no user, it'll tell you to sign in:
So, you 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 your admin page because this page should only be accessed by an adin. but right now, every user and non-logged-in user can access it.
The first thing you need to do is check if there's a user and also if that user is an admin so they can access the admin page. Inside your server.js
, add the authUser
to our admin route. Now it will look like this:
app.get("/admin", authUser,(req, res) => {
res.send("Admin Page");
});
module.exports = { authUser };
To authenticate this to a specific role, you need to write some codes in the basicAuth
to achieve that. So, inside your basicAuth.js
, create a function that takes in the role that you 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();
};
In the code above, 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 };
Remember, we already imported everything we'll need in our server.js
before the start of this tutorial. So, in your server.js
, add the function to the admin route like so:
app.get("/admin", authUser, authRole(ROLE.ADMIN), (req, res) => {
res.send("Admin Page");
});
First, the route is set up to authenticate the user to ensure they are logged in. Then, it checks if the authenticated user has the admin role. If both conditions are met, the route handler runs and allows access to the "Admin Page." Otherwise, the user is denied access.
For user 1
:
For user 2 or 3
:
You'll notice that only admins can now access the route. Now, this should cover most of the authentication needs, but you need to be a little more specific when it comes to specific routes, such as task routes.
Recall that in the data, each user has a specific task that they are linked to, except the admin, who can access every user's task. So you'll need to set up that proper authentication so as not to let any user have access to tasks that are 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,
};
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. To do this, you need to first authenticate that the user exists as you 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");
Then we add it to the task route:
router.get("/:taskId", setTask, authUser, authGetTask, (req, res) => {
res.json(req.task);
});
Notice the authGetTask
? Don't worry, we'll create that shortly. You need to bring in authentication for different permission for the 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();
}
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 2
and user 1
can access it because user 2
is linked to it and user 1 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);
}
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,
};
Inside task.js
, let's change our first routes to this:
router.get("/", authUser, (req, res) => {
res.json(scopedTasks(req.user, tasks));
});
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:
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)
Nice content. Might need some improvement but still nice.