NestJS Role-Based Access

Role-based access control (RBAC) restricts what authenticated users can do based on their assigned role. An admin can delete records; a regular user cannot. A manager can view reports; a viewer cannot. NestJS implements RBAC by combining a custom decorator (to declare required roles on routes) with a guard (to check whether the logged-in user has those roles).

The RBAC Pattern

Route declares required roles → Guard checks user's actual roles → Allow or deny

@Get('reports')
@Roles('admin', 'manager')    ← requires admin OR manager role
getReports() { ... }

User role: 'admin'   → allowed
User role: 'manager' → allowed
User role: 'user'    → 403 Forbidden

Step 1: Define a Role Enum

// role.enum.ts
export enum Role {
  User = 'user',
  Manager = 'manager',
  Admin = 'admin',
}

Using an enum prevents typos. Referring to Role.Admin is safer than the string 'admin' because TypeScript catches invalid values at compile time.

Step 2: Add Role to the User Entity

// user.entity.ts
import { Role } from './role.enum';

@Entity()
export class User {
  @PrimaryGeneratedColumn()
  id: number;

  @Column()
  name: string;

  @Column({ type: 'enum', enum: Role, default: Role.User })
  role: Role;
}

Step 3: Create the Roles Decorator

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from './role.enum';

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

SetMetadata attaches the required roles array to the route handler as metadata. The guard reads this metadata later to know what roles the route demands.

Step 4: Create the Roles Guard

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Role } from './role.enum';
import { ROLES_KEY } from './roles.decorator';

@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 || requiredRoles.length === 0) {
      return true;   // no roles required → open to all authenticated users
    }

    const { user } = context.switchToHttp().getRequest();
    return requiredRoles.some(role => user.role === role);
  }
}

Step 5: Apply Both Guards to Routes

@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)   // JWT first, then roles
export class AdminController {

  @Get('users')
  @Roles(Role.Admin)
  listAllUsers() { ... }

  @Get('reports')
  @Roles(Role.Admin, Role.Manager)
  getReports() { ... }

  @Get('dashboard')   // no @Roles → any authenticated user
  getDashboard() { ... }
}

Full RBAC Flow Diagram

GET /admin/reports
  Authorization: Bearer <token>
         |
         v
  JwtAuthGuard verifies token
  → attaches user { id: 1, role: 'manager' } to req.user
         |
         v
  RolesGuard reads @Roles metadata: ['admin', 'manager']
  → checks req.user.role ('manager')
  → 'manager' is in ['admin', 'manager'] → true
         |
         v
  AdminController.getReports() executes ✓

GET /admin/reports with role 'user':
  → RolesGuard: 'user' not in ['admin', 'manager'] → false → 403 Forbidden

Multiple Roles on One Route

Passing multiple roles to @Roles() means the user must have ANY ONE of those roles. If you need the user to have ALL roles simultaneously, change the some to every in the guard's check:

// ANY one role (default pattern)
return requiredRoles.some(role => user.role === role);

// ALL roles required
return requiredRoles.every(role => user.roles.includes(role));

Public Routes

When you apply JwtAuthGuard globally, every route requires a valid token. Mark public routes with a custom @Public() decorator that the guard reads to skip authentication:

// public.decorator.ts
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);

// jwt.guard.ts
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private reflector: Reflector) { super(); }

  canActivate(context: ExecutionContext) {
    const isPublic = this.reflector.getAllAndOverride<boolean>(
      IS_PUBLIC_KEY,
      [context.getHandler(), context.getClass()],
    );
    if (isPublic) return true;
    return super.canActivate(context);
  }
}

// Usage
@Get('health')
@Public()
healthCheck() { return 'OK'; }

Role-based access is the backbone of any multi-user application. By combining decorators, guards, and JWT claims, NestJS makes it straightforward to define fine-grained access rules directly on the routes they govern — readable, centralized, and easy to extend as your permission requirements evolve.

Leave a Comment

Your email address will not be published. Required fields are marked *