What is Role-Based Access Control?
Role-based access control (RBAC) is an approach used to restrict access to certain parts of the system to only authorized users. The permissions to perform certain operations are assigned to only specific roles. Users of the system are assigned those roles, and through those assignments, they acquire the permissions needed to perform particular system functions. Since users are not assigned permissions directly, but only acquire them through the roles that have been assigned to them, management of individual user rights becomes a matter of simply assigning appropriate roles to a particular user.
With that explained, let’s build a simple user management system/application and use role-based access control to restrict access to certain parts of the application to only users with authorized roles. If needed, you can find the code of the application developed all through this tutorial in this Github repository.
Prerequisites
You’ll need to have a basic understanding of Node.js and Javascript to follow along with this article. It is also required that you have the Node package installed, if you don’t have this you can get it from the official Node.js website, adequate instructions are provided there on how to get it setup. The application will also be using the MongoDB database to store user details, so it’s important you get that set up if you haven’t. There are detailed instructions on the MongoDB website on how to download and set up the database locally.
Scaffolding the Application
Firstly, let’s create a directory for the application, head over to a convenient directory on your system and run the following code in your terminal:
Now, navigate to the created directory and initialize NPM(package manager):
The command above initializes an npm project in the application directory and creates a
package.json
file, this file will hold necessary information regarding the application and also related project dependencies that will be used by the application.
The application we’ll build won’t be complex therefore the directory structure for the application would be simple as well:
Installing the necessary packages
As previously mentioned, we’ll be using some dependencies/packages to help in building parts of our application so let’s go ahead and install them. In your terminal, run the following command:
Here’s a brief rundown of what each installed package actually helps us with:
dotenv
: This package loads environmental variables from a.env
file into Node’sprocess.env
object.bcrypt
: is used to hash user passwords or other sensitive information we don’t want to plainly store in our database.body-parser
: is used to parse incoming data from request bodies such as form data and attaches the parsed value to an object which can then be accessed by an express middleware.jsonwebtoken
: provides a means of representing claims to be transferred between two parties ensuring that the information transferred has not been tampered with by an unauthorized third party, we’ll see exactly how this works later on.mongoose
: is an ODM library for MongoDB, provides features such as schema validation, managing relationships between data, etc…express
: makes it easy to build API’s and server-side applications with Node, providing useful features such as routing, middlewares, etc..accesscontrol
: provides role and attribute-based access control.
It’s perfectly fine if you aren’t familiar with all the packages now. As we go through the article, things will get much clearer and we’ll see exactly what role each package plays in helping us build our application.
Setting up the Database Model
As stated earlier, we’ll be using MongoDB as the preferred database for this application and particularly
mongoose
for data modeling, let’s go ahead and set up the user schema. Head over to the server/models/userModel.js
file and insert the following code:In the file above, we define what fields should be allowed to get stored in the database for each user and also what type of value each field should have. The
accessToken
field will hold a JWT(JSON web token), this JWT contains claims or you could say information that will be used to identify users across the application.
Each user will have a specific role and that’s very important. To keep the application fairly simple, we’ll allow just three roles as specified in the
enum
property, permissions for each role will be defined later on. Mongoose provides a handy default
property that enables us specify what the default value for a field should be if one isn’t specified when a user is created.
With that sorted, let’s set up some basic user authentication.
Setting up User Authentication
To implement role-based access control in our application, we’ll need to have users in our application which we’ll grant access to certain resources based on their roles. So in this section, we’ll set up some logic to handle user signup, login and everything that has to do with authentication. Let’s start with sign up.
User Signup
All authentication and authorization logic will live inside the
server/controllers/userController.js
file. Go ahead and paste the code below into the file and we’ll go through it in detail right after :
Let’s break down the code snippet above, we have two utility functions:
hashPassword
which takes in a plain password value then uses bcrypt
to hash the value and return the hashed value. validatePassword
on the other hand, will be used when logging in to verify if the password is the same with the password the user provided when signing up. You can read more about bcrypt from the official documentation.
Then there’s the signup function, the email and password values will ideally be sent from a form then the
bodyParser
package will parse the data sent through the form and attach it to the req.body
object. The provided data is then used to create a new user. Finally, after the user is created we can use the user’s ID to create a JWT, that JWT will be used to identify users and determine what resources they’ll be allowed to access.
The
JWT_SECRET
environmental variable holds a private key that is used when signing the JWT, this key will also be used when parsing the JWT to verify that it hasn’t been compromised by an authorized party. You can easily create the JWT_SECRET
environmental variable by adding it to the .env
file in the project directory, you can set the variable to any value of your choice:
JWT_SECRET={{YOUR_RANDOM_SECRET_VALUE}}
|
There are multiple functions above prefixed with the async keyword, this is used to indicate that an asynchronous operation using Javascript Promises is going to take place. If you aren’t quite familiar with how Async/Await works, you can read more about it here.
With that done, let’s set up the login logic.
User Login
Let’s also set up user login, go ahead and paste the following code below at the bottom of the
server/controllers/userController.js
file:The code above is very similar to that of signing up. To log in, the user sends the email and password used when signing up, the
validatePassword
function is used to verify that the password is correct. When that’s done, we can then create a new token for that user which will replace any previously issued token. That token will ideally be sent by the user along in the header when trying to access any restricted route.
That’s all for authentication, next we’ll create the three roles previously specified and also define permissions for each role.
Creating roles with AccessControl
In this section, we’ll create specific roles and define permissions on each role for accessing resources. We’ll do this in the
server/roles.js
file, once again copy and paste the code below into that file and we’ll go through it after:All roles and permissions were created using the
Accesscontrol
package, it provides some handy methods for creating roles and defining what actions can be performed by each role, the grant
method is used to create a role while methods such as readAny
, updateAny
, deleteAny
, etc… are called action attributes because they define what actions each role can perform on a resource. The resource, in this case, is profile
. To keep our application simple and to the point, we defined minimal actions for each role.
Inheritance between roles can be achieved using the
extend
method, this allows a role to inherit all attributes defined on another role. The Accesscontrol
package provides a plethora of features and if you want to dig deeper, there’s an in-depth official documentation available.Setting up Routes
Next up, we’ll create routes for parts of our application. Some of these routes contain resources that we want to limit to only users with specific roles.
But before that let’s set up the logic for the routes, functions which will be plugged in as middlewares into the various routes. We’ll be creating functions for retrieving all users, getting a particular user, updating a user and then deleting a user.
Once again, paste the code below to the bottom of the
server/controllers/userController
file:The functions above are quite straightforward and can easily be understood without much explanation. Let’s focus rather on creating middleware for restricting access to only logged in users and also a middleware for allowing access to only users with specific roles.
Once again paste the following code at the bottom of the
server/controllers/userController.js
file:The
allowIfLoggedIn
middleware will filter and only grant access to users that are logged in, the res.locals.loggedInUser
variable holds the details of the logged-in user, we’ll populate this variable very soon.
The
grantAccess
middleware, on the other hand, allows only users with certain roles access to the route. It takes two arguments action
and resource
, action
will be a value such as readAny
, deleteAny
, etc.. this indicates what action the user can perform while resource
represents what resource the defined action has permission to operate on e.g profile
. The roles.can(userRole)[action](resource)
method determines if the user’s role has sufficient permission to perform the specified action of the provided resource. We’ll see exactly how this works next.
Let’s create our routes and plug in the necessary middleware, add the code below to the
server/routes/route.js
file:We’ve created our routes and plugged in the created functions as middleware to enforce certain restrictions on some of these routes. If you look closely at the
grantAccess
middleware you can see we specify that we only want to grant access to roles that are permitted to perform the specified action on the provided resource.
Lastly, let’s add the base server file located at
server/server.js
:In the file above we did some more package configurations, set up what port our server should listen on, used
mongoose
to connect to our local MongoDB server and also configured some other necessary middleware.
There’s an important middleware above and we’ll go through it next:
Remember, a token is sent by the user whenever they want to access a secure route. The above middleware retrieves a token from the
x-access-token
header, then uses the secret key used in signing the token to verify that the token hasn’t been compromised. When that check is complete, the token is then parsed and the user’s ID is retrieved, we also add an extra verification to make sure the token hasn’t expired. When all that is done, the user’s ID is then used to retrieve all other necessary details about the user and that is stored in a variable which can be accessed by subsequent middleware.
Comments
Post a Comment