How to Implement Role-Based Access Control in NestJS with MongoDB - Part 6

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

ยท

7 min read

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.

Screenshot of a DELETE request.  The request URL is "http://localhost:3000/users/1", and the Bearer token is provided for authentication. The response shows a status of "200 OK" and a message indicating "This action removes a #1 user."

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.

Screenshot of a HTTP request error in a web application. The request is a DELETE request to 'http://localhost:3000/users/1' with Bearer token authentication. The response shows a '403 Forbidden' error with a message saying 'Forbidden resource', an 'error' labeled as 'Forbidden', and a 'statusCode' of 403."

We have successfully set up role-based access control in NestJs using MongoDB.

Despicable Me Minions GIF

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

  1. GitHub Repo

  2. https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-1

  3. https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-2

  4. https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-3

  5. https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-4

  6. https://blog.amanpreet.dev/how-to-implement-role-based-access-control-in-nestjs-with-mongodb-part-5

  7. https://courses.nestjs.com/

ย