Back to all posts

Implementing Role-Based Access Control (RBAC) in NestJS

S
sonhp
7 min read

Implementing Role-Based Access Control (RBAC) in NestJS

Authorization is a key security component in modern applications. Role-Based Access Control (RBAC) is a common approach to restricting access to certain resources based on user roles (e.g., admin, user, editor).

In this post, we’ll implement RBAC in NestJS using Guards and Custom Decorators.


1. Understanding Role-Based Access Control (RBAC)

RBAC allows you to define permissions based on roles. Example role hierarchy:

  • Admin – Full access to all resources.
  • Editor – Can create/update content but cannot delete users.
  • User – Can only view content.

Instead of manually checking user roles inside controllers, we can use Guards to enforce access rules dynamically.


2. Defining User Roles in NestJS

Create an enum for user roles:

export enum Role {
  USER = 'user',
  EDITOR = 'editor',
  ADMIN = 'admin',
}

This ensures consistency when assigning and checking roles.


3. Creating a Custom Roles Decorator

Decorators allow us to attach metadata to routes. Let’s create a Roles decorator:

import { SetMetadata } from '@nestjs/common';
import { Role } from '../enums/role.enum';

export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);

This will store role metadata on route handlers.

Usage example:

@Roles(Role.ADMIN)
@Get('admin')
findAllAdmins() {
  return 'This route is only accessible to Admins';
}

4. Implementing a Role-Based Guard

Now, let's create an AuthGuard that checks if a user has the required role.

import {
  Injectable,
  CanActivate,
  ExecutionContext,
  ForbiddenException,
  Reflector,
} from '@nestjs/common';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { Role } from '../enums/role.enum';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<Role[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (!requiredRoles) {
      return true; // No role restriction
    }

    const { user } = context.switchToHttp().getRequest();

    if (!user || !requiredRoles.includes(user.role)) {
      throw new ForbiddenException(
        'You do not have permission to access this resource',
      );
    }

    return true;
  }
}

Key Points:
Extracts role metadata from the request.
Checks if the user has the required role.
Throws ForbiddenException if access is denied.


5. Applying Role-Based Access Control (RBAC) in Controllers

Now, apply the Roles decorator and RolesGuard to routes:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { RolesGuard } from '../guards/roles.guard';
import { Roles } from '../decorators/roles.decorator';
import { Role } from '../enums/role.enum';

@Controller('users')
@UseGuards(RolesGuard) // Apply guard globally to all routes
export class UsersController {
  @Get('profile')
  @Roles(Role.USER, Role.EDITOR, Role.ADMIN)
  getProfile() {
    return 'User Profile';
  }

  @Get('manage')
  @Roles(Role.ADMIN)
  manageUsers() {
    return 'Admin Only: Manage Users';
  }
}

6. Extracting User Roles from JWT

To check user roles dynamically, modify the JwtStrategy:

import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
  constructor() {
    super({
      jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
      secretOrKey: process.env.JWT_SECRET || 'supersecretkey',
    });
  }

  async validate(payload: any) {
    return {
      userId: payload.sub,
      username: payload.username,
      role: payload.role,
    };
  }
}

When a user logs in, their JWT must include their role:

const payload = { username: user.username, sub: user.id, role: user.role };
const token = this.jwtService.sign(payload);

7. Applying RBAC Globally (Optional)

To apply RBAC globally, register RolesGuard at the module level:

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from './guards/roles.guard';

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: RolesGuard,
    },
  ],
})
export class AppModule {}

This ensures all protected routes require role validation.


8. Handling Unauthorized Access & Errors

If a user tries to access a restricted resource, they will receive a 403 Forbidden response:

{
  "statusCode": 403,
  "message": "You do not have permission to access this resource",
  "error": "Forbidden"
}

To customize error messages, modify RolesGuard:

throw new ForbiddenException(
  `Access Denied: ${requiredRoles.join(', ')} role required`,
);

Conclusion

🔹 Role-Based Access Control (RBAC) ensures secure and granular authorization in NestJS.
🔹 Using Guards and Decorators allows for clean, scalable permission management.
🔹 JWT must include user roles to enforce access control dynamically.

With these techniques, you can secure APIs, admin panels, and user dashboards effectively.

S

Written by sonhp

Technical writer and developer passionate about web technologies.

Related Articles