Controlling access to resources based on user roles is a fundamental security practice in modern application development. In NestJS, Guards offer a clean and extensible way to implement Role-Based Access Control (RBAC) at the route or controller level.
This module walks you through how to implement RBAC using custom decorators and guards in NestJS to protect routes based on user roles.
Table of Contents
- What is RBAC?
- How NestJS Guards Help
- Defining User Roles
- Creating a Roles Decorator
- Implementing a Roles Guard
- Using Roles Guard in Controllers
- Combining with JWT Auth Guard
- Best Practices
- Conclusion
What is RBAC?
Role-Based Access Control (RBAC) is a system where each user is assigned a role, and each role has specific permissions. For example:
Admin
: full accessEditor
: can edit but not deleteUser
: read-only access
RBAC ensures that only authorized users can access certain resources or perform specific actions.
How NestJS Guards Help
Guards in NestJS determine whether a request will be handled by the route handler. They are ideal for enforcing authentication and authorization rules.
Guards return true
to proceed or throw an exception (like ForbiddenException
) to deny access.
Defining User Roles
First, define a role enum:
tsCopyEdit// roles.enum.ts
export enum Role {
User = 'user',
Editor = 'editor',
Admin = 'admin',
}
Add a roles
field in your User entity or DTO:
tsCopyEdit// user.entity.ts
export class User {
id: number;
email: string;
password: string;
roles: Role[];
}
Creating a Roles Decorator
Custom decorators allow us to attach metadata to route handlers, which guards can later read.
tsCopyEdit// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { Role } from './roles.enum';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: Role[]) => SetMetadata(ROLES_KEY, roles);
Implementing a Roles Guard
The guard checks the user’s roles against the required roles defined via the decorator.
tsCopyEdit// roles.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
ForbiddenException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
import { Role } from './roles.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 || requiredRoles.length === 0) {
return true; // No role restriction
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user?.roles) {
throw new ForbiddenException('No roles found for user');
}
const hasRole = user.roles.some((role: Role) =>
requiredRoles.includes(role),
);
if (!hasRole) {
throw new ForbiddenException('Access denied');
}
return true;
}
}
Using Roles Guard in Controllers
tsCopyEdit// posts.controller.ts
import { Controller, Get, UseGuards } from '@nestjs/common';
import { Roles } from '../auth/roles.decorator';
import { Role } from '../auth/roles.enum';
import { RolesGuard } from '../auth/roles.guard';
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
@Controller('posts')
@UseGuards(JwtAuthGuard, RolesGuard)
export class PostsController {
@Get('admin')
@Roles(Role.Admin)
findAllForAdmin() {
return 'Only admins can access this route';
}
@Get('editor')
@Roles(Role.Editor, Role.Admin)
findAllForEditor() {
return 'Admins and Editors can access this route';
}
}
Combining with JWT Auth Guard
In a real-world app, the roles are often decoded from the JWT payload. Modify your JWT strategy to include roles in the token:
tsCopyEditasync validate(payload: any) {
return { userId: payload.sub, email: payload.email, roles: payload.roles };
}
Ensure roles are included during login:
tsCopyEditasync login(user: any) {
const payload = {
sub: user.id,
email: user.email,
roles: user.roles,
};
return {
accessToken: this.jwtService.sign(payload),
};
}
Best Practices
- Define roles centrally and document them.
- Avoid hardcoding roles in multiple places.
- Always validate that roles exist before issuing tokens.
- Consider combining RBAC with attribute-based access control (ABAC) for more flexibility in the future.
Conclusion
RBAC using NestJS guards provides a powerful and declarative way to protect routes and resources based on user roles. By combining custom decorators, guards, and JWT-based authentication, you can build scalable and secure APIs.