4.4.1 Update backend to implement Role Based Access Control (RBAC), add CORS and update logout feature

Introduction

Before diving into frontend authentication, it’s essential to establish a strong foundation by implementing Role-Based Access Control (RBAC) on the backend. This approach ensures that the basics of authentication and RBAC are developed simultaneously, providing a secure and efficient authentication system. This article guides you through the steps to create roles, guards, and necessary endpoints, as well as updating the frontend to leverage this RBAC setup. Additionally, we will update main.ts to configure CORS and enhance our backend logout feature with the developed Logout interceptor

Role based access on backend

To develop basics of RBAC we have to create Roles decorator Roles guard, create ‘users’ API endpoint for Admin role in UsersController for example and update

Generate Roles decorator

nx g @nx/nest:decorator roles --directory=libs/backend/utils/src/lib/decorators --nameAndDirectoryFormat=as-provided 

Update generated Roles

import { SetMetadata } from '@nestjs/common';
import { RoleEnum } from  '@mtfs/shared/enums';

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

Generate Roles guard

nx g @nx/nest:guard roles --directory=libs/backend/features/src/lib/roles/guards --nameAndDirectoryFormat=as-provided 

Update libs/backend/features/src/lib/roles/guards/roles.guard.ts:

import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { RoleEnum } from '../role.enum';
import { User } from '../../users/entities/user.entity';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { ROLES_KEY } from '../../../../../utils/src/lib/decorators/roles.decorator';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(
    context: ExecutionContext
  ): boolean  {
    const requiredRoles = this.reflector.getAllAndOverride<RoleEnum[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if(!requiredRoles){
      return true;
    }
    const request = context.switchToHttp().getRequest();
    const user = request?.user as User;
    return requiredRoles.some((role) => user.role?.name === role);
  }
}

We have two Roles User and Admin. To check RolesGuard we set up ‘users’ API endpoint to get access only for Admin role.

Update UsersContoller (libs/backend/features/src/lib/users/users.controller.ts)

import {
  Controller,
  Get,
  UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service'
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { RoleEnum } from '@mtfs/shared/enums';
import { Roles } from '@mtfs/backend/utils';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { RolesGuard } from '../roles/guards/roles.guard';

@Controller('users')
@ApiTags('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Get()
  @ApiOperation({summary: 'Get all user'})
  @ApiResponse({status: 200, description: 'List of users retrieved successfully'})
  @Roles(RoleEnum.Admin)
  @UseGuards(JwtAuthGuard, RolesGuard)
  findAll() {
    return this.usersService.findAll();
  }
}

CORS configuration

Also we have to update main.ts to configure CORS:


  app.enableCors({    
    origin: '<http://localhost:4200>',
    methods: ['GET', 'POST', 'PATCH', 'PUT', 'DELETE'],
    credentials: true,
    preflightContinue: false,
    optionsSuccessStatus: 204,
    allowedHeaders: 'Content-Type, Authorization', 
  });

Update Logout feature on backend

Generate LogoutGuard

nx g @nx/nest:guard logout \\
	 --directory=libs/backend/features/src/lib/auth/guards \\
	 --nameAndDirectoryFormat=as-provided 

Update LogoutGuard

import { CanActivate, ExecutionContext, Injectable, Logger, UnauthorizedException } from '@nestjs/common';
import { AuthGuard, IAuthGuard } from '@nestjs/passport';
import { Observable } from 'rxjs';
import { User } from '../../users/entities/user.entity';
import { AuthService } from '../auth.service';

@Injectable()
export class LogoutGuard extends AuthGuard('jwt-refresh-access-token') implements IAuthGuard {
  constructor(
    private authService: AuthService
  ) {
    super();
  }
  private readonly logger = new Logger(LogoutGuard.name)

  override canActivate(
    context: ExecutionContext
  ){
    this.logger.debug(`${LogoutGuard.name} canActivate`);  
    return super.canActivate(context);
  }

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
   override handleRequest(err: unknown, user: User): any {
    this.logger.debug(`${LogoutGuard.name} handleRequest`);  
    if (err || !user) {
      console.log(err);
      console.log(user);
      throw err || new UnauthorizedException();
    }else {
      this.logger.debug(`${LogoutGuard.name} removeCurrentRefreshToken`); 
      this.authService.removeCurrentRefreshToken(user.id);
    }  
    return user;
  }
}

update AuthController

  @Get('logout')
  @ApiOperation({ summary: 'Log out and remove refresh token' })
  @ApiResponse({ status: 200, description: 'Logout successful' })
  @UseGuards(LogoutGuard)
  @HttpCode(HttpStatus.OK)
  @UseInterceptors(ClearCookiesInterceptor)
  async logout(@CurrentUser() user: User) {
    console.log('LOGOUT')  
    if (user) {
      this.authService.removeCurrentRefreshToken(user.id);
    }
    return { succes: true };
  }

Summary and Conclusion

In this article, we covered  simpe way of the implementation of Role-Based Access Control (RBAC) in a NestJS backend and its integration with a frontend application. We began by creating a Roles decorator and a Roles guard to handle role-based access on the backend. We set up an API endpoint in the UsersController to restrict access to users with the Admin role and updated the main.ts file to configure CORS for secure communication between the frontend and backend.

Additionally, we developed a LogoutGuard to handle the logout process more securely. The LogoutGuard ensures that the refresh token is removed when a user logs out, enhancing the security of our application.

By following these steps, we successfully integrated RBAC into both the backend and frontend, creating a more secure and robust authentication system. This comprehensive approach ensures that only authorized users can access specific parts of the application, improving the overall security and user experience.

Now, we are ready back to the fronted app to develop frontend authentication with RBAC.


Discover more from More Than Fullstack

Subscribe to get the latest posts sent to your email.

Leave a Comment