birdgang
6 min readAug 11, 2024

Role-Based Access Control (RBAC) and Its Implementation in NestJS

  1. What is Role-Based Access Control (PBAC)?

Role-Based Access Control (RBAC) is an approach to managing access within a system based on the roles that a user has. RBAC is particularly useful in systems where managing permissions is crucial, as it allows users to have multiple roles, each of which defines what resources they can access and what actions they can perform.

2. Implementing RBAC in NestJS

NestJS, with its powerful module-based architecture and decorators, makes it easy to implement RBAC. This allows you to effectively manage and maintain role-based access control.

To implement RBAC in NestJS, we need the following components :

  • Role Enum: Define the roles used in the system.
  • Roles Decorator: Assign roles to controllers or specific handlers.
  • RolesGuard: A guard that compares the requested roles with the user’s roles and controls access.
  • AuthService and JwtStrategy: Handle authentication and token-based user validation.

3. RBAC Example Implementation in NestJS

3.1. Role Enum
Define roles as an Enum to clearly distinguish all the roles that can be used in the system.

export enum Role {
User = 'User',
Admin = 'Admin',
SuperAdmin = 'SuperAdmin',
}

3.2. Roles Decorator
The decorator is used to set the required roles for specific endpoints.

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

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

3.3. RolesGuard
The RolesGuard validates whether the user has the necessary roles to access a particular endpoint.

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) {
return true;
}

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

return requiredRoles.some((role) => user.roles?.includes(role));
}
}

3.4. AuthService and JwtStrategy
AuthService handles user authentication and JWT token issuance. JwtStrategy validates this JWT token and extracts the roles to pass to the system.

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { InjectEntityManager } from '@nestjs/typeorm';
import { EntityManager } from 'typeorm';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';
import { User } from '../user/user.entity';
import { LoginUserDto } from './dto/login-user.dto';
import { RegisterUserDto } from './dto/register-user.dto';

@Injectable()
export class AuthService {
constructor(
@InjectEntityManager()
private entityManager: EntityManager,
private jwtService: JwtService,
) {}

async login(loginUserDto: LoginUserDto) {
const user = await this.entityManager.createQueryBuilder(User, 'user')
.where('user.email = :email', { email: loginUserDto.email })
.getOne();

if (user && (await bcrypt.compare(loginUserDto.password, user.password))) {
const payload = { username: user.email, sub: user.id, roles: user.roles };
return {
access_token: this.jwtService.sign(payload),
};
}
throw new UnauthorizedException('Invalid credentials');
}
}

JwtStrategy is used to validate the JWT and pass the authenticated user's roles to the system.

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

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private authService: AuthService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: 'your-secret-key',
});
}

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

3.5. Using RolesGuard in a Controller
Let’s implement a controller where role-based access control is applied.

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

@Controller('admin')
@UseGuards(AuthGuard('jwt'), RolesGuard)
export class AdminController {
@Get()
@Roles(Role.Admin)
findAll() {
return 'This route is restricted to Admins only';
}

@Get('super')
@Roles(Role.SuperAdmin)
findSuperAdminData() {
return 'This route is restricted to SuperAdmins only';
}
}

4. The Flow of Role-Based Access Control

  • The user sends a request with a JWT token.
  • JwtStrategy validates the token and extracts the roles of the user.
  • RolesGuard checks whether the user has the necessary roles to access the requested endpoint.
  • If the roles are valid, the request is allowed; otherwise, access is denied.

Sequence Diagram

  • User: The actor that initiates a request to the server.
  • JwtStrategy: The component responsible for validating the JWT (JSON Web Token) and extracting roles from it.
  • RolesGuard: The guard that checks if the user’s roles allow them to access the requested resource.
  • RolesDecorator: A decorator that retrieves the metadata about the roles required for accessing a specific endpoint.
  • AuthService: The service responsible for validating the user’s roles against the data stored in the database.
  • User Entity: Represents the user data stored in the database.

Step-by-Step Breakdown of the Sequence Diagram

  1. User Sends Request with JWT
  • Action: The user sends a request to the server, which includes a JWT in the authorization header.
  • Purpose: The JWT token is used to authenticate the user and verify their identity without requiring them to log in again.

2. JwtStrategy Validates the JWT

  • Action: The JwtStrategy component receives the request and validates the JWT.
  • Purpose: This step ensures that the JWT is legitimate, has not expired, and has not been tampered with.

3. JwtStrategy Extracts Roles from JWT

  • Action: After validating the token, JwtStrategy extracts the roles assigned to the user from the JWT payload.
  • Purpose: These roles determine what resources the user is allowed to access within the application.

4. JwtStrategy passes Roles to RolesGuard

  • Action: The roles extracted from the JWT are then passed to the RolesGuard.
  • Purpose: RolesGuard is responsible for ensuring that the user’s roles match the required roles for accessing the specific endpoint they are requesting.

5. RolesGuard Retrieves Required Roles for Endpoint from RolesDecorator

  • Action: RolesGuard queries the RolesDecorator to retrieve the roles required for the endpoint the user is trying to access.
  • Purpose: RolesDecorator stores metadata that defines which roles are required for different endpoints in the application.

6. RolesDecorator Returns the Required Roles

  • Action: RolesDecorator retrieves and returns the roles metadata back to RolesGuard.
  • Purpose: This allows RolesGuard to compare the user’s roles against the required roles for the endpoint.

7. RolesGuard Validates User’s Roles Using AuthService

  • Action: RolesGuard may call AuthService to further validate the user’s roles by fetching additional user data from the database.
  • Purpose: This step is crucial for verifying that the user’s roles stored in the JWT are consistent with what’s stored in the database, ensuring the roles haven’t been altered.

8. AuthService Fetches User Data from User Entity

  • Action: AuthService queries the User Entity to fetch the relevant user data, including their roles.
  • Purpose: Accessing the database allows AuthService to validate the user’s roles accurately.

9. User Entity Returns User Data to AuthService

  • Action: The User Entity returns the user data to AuthService.
  • Purpose: This provides AuthService with the necessary data to confirm the validity of the user’s roles.

10. RolesGuard Checks If User Has Required Role

  • Action: At this point, RolesGuard checks whether the user has the required roles to access the endpoint.
  • Purpose: The core purpose of RolesGuard is to enforce role-based access control, ensuring only users with appropriate roles can proceed.
  • Outcome: If the user has the required role, the request is allowed; otherwise, the request is denied and the user is notified of insufficient permissions.

To Summarize

  • User Has Required Role: If the user’s roles match the required roles, RolesGuard allows the request to pass through. The JwtStrategy then finalizes the request, and the user gains access to the requested resource. The sequence ends with the system granting access.
  • User Does Not Have Required Role: If the user’s roles do not meet the required criteria, RolesGuard denies the request. The user receives a response indicating that access is denied, and the sequence ends without the user accessing the resource.