In NestJS, guards are used to protect routes by determining whether the request should be allowed to proceed. Guards can be used for a variety of purposes, including role-based access control (RBAC), authentication, and authorization. In this module, we will explore how to use guards to implement role-based access control (RBAC) in a NestJS application.
Table of Contents
- Introduction
- What are Guards in NestJS?
- Creating a Role-Based Access Control Guard
- Using Guards to Protect Routes
- Implementing RBAC: Role-Based Permissions
- Testing Guards
- Best Practices for Using Guards
- Conclusion
Introduction
Role-Based Access Control (RBAC) is a method of restricting access to system resources based on the roles assigned to users. Implementing RBAC is essential for managing access to different parts of your application, ensuring that only users with the correct role can access certain routes.
In this module, we will create a Guard that enforces role-based access control by verifying whether the current user has the appropriate role for accessing a particular route.
What are Guards in NestJS?
In NestJS, guards are classes that implement the CanActivate
interface. They are responsible for determining whether a request should be allowed to proceed. Guards are executed before the route handler is called, making them a great choice for authentication and authorization checks.
A guard’s canActivate()
method is called for every request to a route. The method returns a boolean indicating whether the request should proceed or not. If the guard returns true
, the request is allowed to continue. If it returns false
, the request is denied.
Key Features of Guards:
- Guards can be used globally or applied to specific routes.
- They are commonly used for authentication, authorization, and validation.
- Guards can be executed before the route handler is invoked.
Creating a Role-Based Access Control Guard
To implement RBAC, we need to create a guard that checks the user’s role and determines if they are allowed to access a given route. This can be done by implementing the CanActivate
interface.
Step 1: Create the Role Guard
Let’s create a guard that checks whether the current user has the required role for a route.
Create the Role Guard
typescriptCopyEditimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly requiredRoles: string[]) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
const user = request.user; // Assuming user info is attached to the request by an authentication middleware
if (!user) {
return false; // If the user is not authenticated, deny access
}
// Check if the user has at least one of the required roles
return this.requiredRoles.some(role => user.roles.includes(role));
}
}
In this example:
- The
RolesGuard
class implements theCanActivate
interface. - The
requiredRoles
array specifies the roles that are allowed to access the route. - The guard checks if the current user has at least one of the required roles in their
roles
array. - If the user is authenticated and has one of the roles, the guard returns
true
to allow access; otherwise, it returnsfalse
to deny access.
Step 2: Apply the Guard to a Route
To apply the RolesGuard
to a route, we need to use the @UseGuards()
decorator and pass in the required roles.
typescriptCopyEditimport { Controller, Get, UseGuards } from '@nestjs/common';
import { RolesGuard } from './roles.guard';
@Controller('admin')
export class AdminController {
@Get()
@UseGuards(RolesGuard)
getAdminContent() {
return 'This is the admin content';
}
}
Step 3: Passing the Required Roles to the Guard
You can pass the required roles dynamically using a factory function. This allows different routes or controllers to specify different roles.
typescriptCopyEditimport { SetMetadata, UseGuards } from '@nestjs/common';
import { RolesGuard } from './roles.guard';
// Custom decorator to set roles metadata
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// Applying the Roles decorator to the route
@Controller('admin')
export class AdminController {
@Get()
@Roles('admin') // Only accessible by users with the 'admin' role
@UseGuards(RolesGuard)
getAdminContent() {
return 'This is the admin content';
}
}
In this case, the Roles
decorator is used to attach metadata to the route. The RolesGuard
will then check whether the user has one of the roles specified by the decorator.
Step 4: Extracting Metadata in the Guard
To extract the roles metadata inside the guard, you can use the Reflector
service provided by NestJS:
typescriptCopyEditimport { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';
import { Reflector } from '@nestjs/core';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
if (!roles) {
return true; // If no roles are specified, allow access
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user) {
return false; // If the user is not authenticated, deny access
}
return roles.some(role => user.roles.includes(role));
}
}
In this updated RolesGuard
:
- The
Reflector
service is used to retrieve the roles metadata attached to the route handler. - If no roles are specified, the guard allows access by default.
- The guard checks if the user’s roles match any of the required roles.
Implementing RBAC: Role-Based Permissions
To implement more sophisticated role-based permissions, you can define roles and permissions in your database and use them to control access to different parts of your application.
For example, a user might have roles like admin
, user
, and moderator
, and each role might have different permissions, such as read
, write
, or delete
. You can extend the RolesGuard
to check permissions for more granular control.
Step 1: Define Permissions in the Database
Assume you have a user model with roles and permissions:
typescriptCopyEditexport class User {
id: number;
name: string;
roles: string[];
permissions: string[];
}
Step 2: Modify the Guard to Check Permissions
You can modify the RolesGuard
to also check for specific permissions based on the user’s role:
typescriptCopyEdit@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const requiredPermissions = this.reflector.get<string[]>('permissions', context.getHandler());
if (!requiredPermissions) {
return true; // If no permissions are specified, allow access
}
const request = context.switchToHttp().getRequest();
const user = request.user;
if (!user || !user.permissions) {
return false; // If the user does not have permissions, deny access
}
return requiredPermissions.some(permission => user.permissions.includes(permission));
}
}
Step 3: Apply Permissions Guard
You can now apply the permissions guard to your routes or controllers, specifying which permissions are required:
typescriptCopyEdit@Controller('posts')
export class PostController {
@Get()
@Roles('admin')
@SetMetadata('permissions', ['read'])
@UseGuards(RolesGuard, PermissionsGuard)
getPosts() {
return 'List of posts';
}
}
Testing Guards
Testing guards is an important part of ensuring the correct implementation of your RBAC system. You can use unit tests to verify that the guard behaves as expected. You can mock the user object and the context to simulate various scenarios where the user has or doesn’t have the correct roles or permissions.
Best Practices for Using Guards
- Use Guards for Authorization, Not Authentication: Guards should be used primarily for authorization (role and permission checks) rather than authentication (verifying user identity).
- Use Custom Decorators for Clean Code: Use custom decorators like
@Roles()
and@Permissions()
to make your code cleaner and more maintainable. - Test Your Guards: Always write tests to verify that your guards are correctly enforcing access control rules.
- Handle Errors Gracefully: Guards should not only deny access but should also handle errors gracefully by sending appropriate error responses, such as
403 Forbidden
or401 Unauthorized
.
Conclusion
In this module, we’ve learned how to implement role-based access control (RBAC) in a NestJS application using guards. Guards are a powerful tool for enforcing authorization rules based on roles and permissions. By using guards effectively, you can control access to different parts of your application and ensure that only authorized users can access sensitive data or actions.
Now that you have the foundational knowledge, you can start implementing RBAC in your NestJS applications to improve security and manage user access more effectively.