Implementing Role-Based Access Control (RBAC) in NestJS
Role-Based Access Control (RBAC) is a method used to restrict system access to authorized users based on their roles within an organization. With RBAC, permissions are granted based on the roles a user holds, ensuring a more secure and structured access management.
NestJS, a progressive Node.js framework for building efficient and scalable server-side applications, offers robust support for implementing RBAC. In this article, we will walk through the process of setting up RBAC in a NestJS application using TypeScript.
Setting Up NestJS Project
First, we need to create a new NestJS project. Open your terminal and run the following command:
nest new nestjs-rbac
cd nestjs-rbac
Once our project is set up, we’ll generate a Users module and an Auth module to handle authentication and role management.
nest generate module users
nest generate service users
nest generate controller users
nest generate module auth
nest generate service auth
nest generate controller auth
Defining User Roles
In our Users module, we’ll define a set of roles. In a real-world application, these might be stored in a database, but for simplicity, we’ll hard-code them here.
Create a new file called roles.enum.ts
in the src/users
directory:
// src/users/roles.enum.ts
export enum UserRole {
ADMIN = 'admin',
USER = 'user',
GUEST = 'guest',
}
Creating a User Entity
Next, we’ll create a User entity that includes a role
property. We will store users and their roles in a simple in-memory array for this example.
Create a new file called user.entity.ts
in the src/users
directory:
// src/users/user.entity.ts
import { UserRole } from './roles.enum';
export class User {
id: number;
username: string;
password: string; // Note: In a real application passwords should be hashed
role: UserRole;
}
Now, modify the UsersService to manage users:
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { User } from './user.entity';
import { UserRole } from './roles.enum';
@Injectable()
export class UsersService {
private readonly users: User[] = [
{ id: 1, username: 'admin', password: 'adminpass', role: UserRole.ADMIN },
{ id: 2, username: 'user1', password: 'user1pass', role: UserRole.USER },
];
findOne(username: string): User | undefined {
return this.users.find(user => user.username === username);
}
}
Implementing Auth Guard with Roles
We need an Auth Guard to protect our routes based on user roles. Create a new file called roles.guard.ts
in the src/auth
directory:
// src/auth/roles.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { UserRole } from '../users/roles.enum';
import { UsersService } from '../users/users.service';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector, private usersService: UsersService) {}
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const roles = this.reflector.get<UserRole[]>('roles', context.getHandler());
if (!roles) {
return true;
}
const request = context.switchToHttp().getRequest();
const user = request.user;
const foundUser = this.usersService.findOne(user.username);
return foundUser && roles.includes(foundUser.role);
}
}
Using Role-Based Decorators
We will create a custom decorator to specify roles required by a route:
Create a new file called roles.decorator.ts
in the src/auth
directory:
// src/auth/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
import { UserRole } from '../users/roles.enum';
export const Roles = (...roles: UserRole[]) => SetMetadata('roles', roles);
Applying RBAC to Routes
Let’s secure some routes by applying our Roles
decorator and RolesGuard
:
Modify users.controller.ts
to include routes protected by roles:
// src/users/users.controller.ts
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { UsersService } from './users.service';
import { Roles } from '../auth/roles.decorator';
import { UserRole } from './roles.enum';
import { RolesGuard } from '../auth/roles.guard';
@Controller('users')
@UseGuards(RolesGuard)
export class UsersController {
constructor(private readonly usersService: UsersService) {}
@Get('profile')
@Roles(UserRole.USER, UserRole.ADMIN)
getProfile(@Request() req) {
return req.user;
}
@Get('admin')
@Roles(UserRole.ADMIN)
getAdminData() {
return 'Admin Data';
}
}
Don’t forget to register the RolesGuard
as a provider for your module:
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { APP_GUARD } from '@nestjs/core';
import { RolesGuard } from '../auth/roles.guard';
@Module({
controllers: [UsersController],
providers: [
UsersService,
{
provide: APP_GUARD,
useClass: RolesGuard,
},
],
})
export class UsersModule {}
Finally, we need some form of authentication to set req.user. For simplicity, you can use a very basic middleware or use libraries like Passport.
Conclusion
We’ve implemented a basic RBAC system in a NestJS application using TypeScript. This system allows us to control access to routes based on user roles, ensuring that only authorized users can access restricted areas of our application.
This is a foundational step towards securing your NestJS applications, and you can extend this approach by integrating more sophisticated authentication mechanisms and storing roles in a database.
Happy coding! 🚀