SWASTIJ CONSULTANCY

  1. Introduction

    In this article, I will show you how to implement RBAC in NestJS using NATS.

    We will create a simple application with two roles: admin and user. The admin role will have full access to the system, while the user role will only be able to view data.

    But first, let us clear some basics.

    Why NATS?

    • * It fits the microservice architecture.
    • * lightweight
    • * flexible
    • * highly performant
    • * considering factors like load balancing and service discovery, it makes it easier to create microservices with NATS.
    • * Data shows that for the execution of requests sent serially, NATS takes much less time than HTTP.

    RBAC

    Role-based access control (RBAC) is a method of restricting network access based on the roles of individual users within an enterprise. Organizations use RBAC also called role-based security to parse access levels based on an employee's roles and responsibilities.

    Role-based system access (RBAC) is a security mechanism that allows you to control who has access to what resources in your system. With RBAC, you can define roles that have different permissions, and then assign users to those roles. This allows you to fine-grained control over who can access your system and what they can do.

  2. Role-Based System Access in NestJS Using NATS

NestJS is a framework for building scalable, efficient, and easy-to-maintain web applications. NATS is a lightweight messaging system that can be used to decouple different parts of your application.

Let's create a simple application with two roles: admin and user. The admin role will have full access to the system, while the user role will only be able to view data.

Getting Started

The first step is to create a new NestJS project. We can do this by running the following command:

 nest new project-name

I have used nest-access-control library to implement RBAC in my microservice architecture.


To implement RBAC in NestJS using NATS, you will need to do the following:

Here is an example of how to define roles and permissions:

 //app.roles.ts

import { RolesBuilder } from 'nest-access-control/roles-builder.class';

export enum AppRoles {
    USER = 'user',
    ADMIN = 'admin',
}

export const roles: RolesBuilder = new RolesBuilder();

roles
    .grant(AppRoles.USER) // define new or modify existing role. also takes an array.
    .createOwn('user')
    .readOwn('user')
    .grant(AppRoles.ADMIN)
    .createAny('user')
    .readAny('user')
    .readAny('business-info')
    .createAny('business-info');

Creating Role Guards and Decorators

Now that we’ve defined our roles and permissions using RolesBuilder, the next step is to protect specific routes or actions based on the assigned roles. This is where custom decorators and guards come in.

📌 Step 1: Create a @InjectRolesBuilder( ) Decorator

 //inject-roles-builder.decorator.ts

import { Inject } from '@nestjs/common';
import { ROLES_BUILDER_TOKEN } from 'nest-access-control/constants';

/**
 *  Get access to the underlying `RolesBuilder` Object
 */
export const InjectRolesBuilder = () => Inject(ROLES_BUILDER_TOKEN);

🛡️ Step 2: Create a RolesGuard - "ACGuard"

 //acguard.guard.ts

import {
    Injectable,
    CanActivate,
    ExecutionContext,
    UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { IQueryInfo } from 'accesscontrol';
import { InjectRolesBuilder } from 'src/decorators/inject-roles-builder.decorator';
import { Role } from 'nest-access-control/role.interface';
import { RolesBuilder } from 'nest-access-control/roles-builder.class';
import UserDb from 'src/mock/user';

@Injectable()
export class ACGuard<User extends any = any> implements CanActivate {
    constructor(
        private readonly reflector: Reflector,
        @InjectRolesBuilder() private readonly roleBuilder: RolesBuilder
    ) {}

    protected async getUser(context: ExecutionContext): Promise<User> {
        const user = context.switchToRpc().getData().user;
        return user;
    }

    protected async getUserRoles(
        context: ExecutionContext
    ): Promise<string | string[]> {
        const user: any = await this.getUser(context);
        if (!user) throw new UnauthorizedException();
        const userRole = UserDb.find((u) => u.id == user.id).roles;
        return userRole;
    }

    public async canActivate(context: ExecutionContext): Promise<boolean> {
        const roles = this.reflector.get<Role[]>('roles', context.getHandler());
        if (!roles) {
            return true;
        }

        const userRoles = await this.getUserRoles(context);
        const hasRoles = roles.every((role) => {
            const queryInfo: IQueryInfo = role;
            queryInfo.role = userRoles;
            const permission = this.roleBuilder.permission(queryInfo);
            return permission.granted;
        });
        return hasRoles;
    }
}

✅ Don’t forget to register this guard globally or apply it using @UseGuards(RolesGuard) on protected routes.

🧵 Wiring It All Together with NATS

With RBAC now in place at the HTTP layer, let’s talk about how it integrates with your microservices via NATS.

In a microservice architecture, the gateway receives the HTTP request and communicates with other services (like auth, user, product) via NATS.

For example, from the gateway, you could do:

 // src/gateway/app.controller.ts

  @Get('/me')
  async getCurrentUser(@Req() req: any, @Response() response: any) {
    try {
      const res = await lastValueFrom(
        this.userClient.send('me', { headers: req.headers }),
      );
      return response.status(res.code || res.status || 200).json(res);
    } catch (e) {
      console.log('error', e);
      return response.status(e.code || e.status || 500).json(e.message);
    }
  }

In the user service, you’d have:

 // src/users/users.controller.ts

  @UseInterceptors(ClassSerializerInterceptor)
  @UseGuards(AuthMiddleware)
  @MessagePattern('me')
  async getCurrentUser(data: any) {
    return await this.appService.handleGetCurrentUser(data.user);
  }

NOTE: Alternatively, depending on your requirements and business logic, you can have the ACGuard at the gateway itself.

Option 2 - ACGuard at gateway

In this case, your gateway will be

 // src/users/users.controller.ts

@Get('me')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('user', 'admin')
async getMyUser(@Req() req) {
  return this.natsClient.send('get-user', req.user.id);
}

and in user service, you'll have

 // src/users/users.service.ts

@MessagePattern('get-user')
getUser(@Payload() userId: string) {
  return this.userRepository.findOne({ where: { id: userId } });
}

🧪 Testing the Flow

Once your services are up, test the full flow:

You now have a fully functioning, role-based microservice architecture using NestJS and NATS! 🎉