How to Implement Role-Based Access Control in NestJS with MongoDB - Part 6
Set up roles, permissions, and secure user access in NestJS with MongoDB using this detailed guide on Role-Based Access Control
Introduction
This post is the sixth and the last in a series titled "Implement Role-Based Access Control in NestJS using MongoDB."
In this series, we'll create an RBAC (Role-Based Access Control) app from scratch using NestJS and MongoDB. Whether you're a beginner or looking to implement role-based access control in your app, follow along and build the app with me!
Previous Post
In the last post, we worked on the following things.
Implementing Refresh Tokens
Storing Refresh Tokens in a key-value store DB such as Redis.
Invalidating Tokens after its subsequent use
Check out the blog post here
What's Next?
In this post, we will cover the following:
What is Authorization?
Implement Role-based Access Control authorization in our app.
What is Authorization?
Authorization is the process of giving a user permission to access a resource. Authorization is independent from Authentication but it always requires an Authentication mechanism to be implemented.
Types of Authorization
Role-based Access Control is a policy-neutral access-control mechanism defined around roles and privileges.
Claim-based Access Control - Instead of defining a set of roles that can be assigned to users, we define multiple permissions and then grant those permissions to individual users.
Policy-based Access Control - A Policy defines a requirement or a collection of requirements that the user must satisfy to access a resource. In other words, both Role-based and Claim-based access are forms of Policy-based access, but they are hardcoded.
Implementing Role-Based Authorization
As updated earlier Role-based authorization is a control mechanism defined around roles and permissions. When using Role-based Access control, we need to examine the needs of our users and group them into specific roles based on their responsibilities.
Let's take an example using our User
entity. Suppose we want to prevent a regular user from accessing the Delete
API, allowing only admins or moderators to use that API.
How are we going to do that? We will explore this in the upcoming section, so stay tuned.
Roles
There are various ways to store roles based on the application's needs. We can save them in the database, or we can have a separate dashboard to manage roles as a distinct entity.
In this post, we are going to consider a predefined and static list of Roles. You can change them as per your application requirements.
For our application, we are considering three roles attached to our User
entity: Admin, Moderator, and User. Admin will have all the permissions, while Moderator and User will have more limited permissions.
Follow these steps to create Roles for our application.
First, create a new folder named enums
under the users
folder. Then, create a file named roles.enum.ts
in that folder.
mkdir src/users/enums && cd src/users/enums
touch role.enum.ts
Add the following content to the newly created file.
// role.enums.ts
export enum Role {
Admin = 'admin',
Moderator = 'moderator',
User = 'user',
}
Refactoring User entity and interface.
Since role
is a property of User
entity, let's refactor our User
entity.
// user.entity.ts
export class User extends Document {
...
...
@Prop({ enum: Role, default: Role.User })
role: Role;
}
By default whenever a new user is registered, the role user
will be associated. If you already have users in your database, you can update them using the below Mongo query.
db.users.updateMany({}, {$set: {role: "user"} })
Now we also need to add this role
property to our JWT signed token and also to the active-user.data.interface.ts
file.
// active-user.data.interface.ts
export interface ActiveUserData {
sub: number;
email: string;
role: Role; // ๐
}
// auth.service.ts
async generateTokens(user: User) {
...
...
const [accessToken, refreshToken] = await Promise.all([
this.signToken<Partial<ActiveUserData>>(
user.id,
this.jwtConfiguration.accessTokenTtl,
{ email: user.email, role: user.role }, // ๐
),
this.signToken(user.id, this.jwtConfiguration.refreshTokenTtl, {
refreshTokenId,
}),
]);
...
...
}
Now the accessToken
generated by JWT will also include the role
property in its payload.
Creating a Roles Decorator
Next step we create a new decorator @Roles
by using the below command.
nest g d iam/decorators/roles --flat
In the above file created, let's first declare the metadata key.
// roles.decorator.ts
export const ROLES_KEY = 'roles';
Next, we adjust our code for the custom @Role()
decorator as follows:
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from 'src/users/enums/role.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
Adding a custom Role Guard
Next, we create a new Roles
guard by executing the following command in our terminal using NestJs CLI
nest g guard iam/guards/role
In this guard, we need to inject the Reflector
dependency into our constructor()
, the Reflector
provider gives us access to the underlying metadata.
// role.guard.ts
export class RoleGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
...
...
}
In the canActivate()
method of the class, we start by retrieving the required roles for the target endpoints using the provider Reflector
.
// role.guard.ts
...
...
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const contextRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!contextRoles) {
return true;
}
const user: ActiveUserData = context.switchToHttp().getRequest()[
REQUEST_USER_KEY
];
return contextRoles.some((role) => user.role === role);
}
If no roles are required for a target endpoint, we assume no further validation is needed and proceed. Otherwise, we need to take the ActiveUserData
from the request, which we assigned using the AccessTokenGuard
, and check if it has one of the required roles.
Now let's register the RoleGuard
globally by adding it to the IamModule
as a provider.
// iam.module.ts
...
providers: [
...
{ provide: APP_GUARD, useClass: RoleGuard }, // ๐
...
],
...
The last step is to mark methods that need specific roles with our new @Roles()
decorator.
Let's open our UserController
and annotate remove()
method with @Roles()
decorator.
// users.controller.ts
...
@Roles(Role.Admin)
@Delete(':id')
remove(@Param('id') id: string) {
return this.usersService.remove(+id);
}
To test the above API endpoint make sure you are adding the accessToken
in the authorization header of your request otherwise, you will receive 401
error code.
In case you are getting the 401 error, please use the auth/sign-in
API endpoint to get the accessToken
. Grab the accessToken
and add it to the authorization header of your request.
Play with different roles available in your application and see how the access control is managed.
In case the user Role which you have used to get the access token is allowed to access the API endpoint you will get a response like shown in the below image.
Otherwise, if the user Role is not allowed to access the API endpoint, then you will get an error response with a 403 error code shown in the below image.
We have successfully set up role-based access control in NestJs using MongoDB.
Conclusion
We covered a lot in this post, so let's summarize:
What is Authorization?
Types of Authorization
Implementing Role-based Access control by creating a custom decorator and guard.
That's it for today and congratulations to everyone who has followed this series of tutorials for successfully creating a Role-based Access control application in NestJs using MongoDB.
All the code related to the above series is been added as a GitHub template repository, link shared in the References section. Go and create your new project using the template with Role-based Access Control already integrated and configured with your favourite framework NestJS and MongoDB as a database.
I hope you have learned something new as I did. If so, kindly like and share the article and also follow me to read more exciting articles. You can check my social links here.
All the references to the published series have been added in the next section "References".
References
https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-1
https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-2
https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-3
https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-4
https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-5