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.