Home > Net >  Nestjs Roles Decorator: Crazy behaviour with getting user out of payload data
Nestjs Roles Decorator: Crazy behaviour with getting user out of payload data

Time:11-27

I am just trying to implement my Roles Decorator in Nestjs. So far it went pretty well, until I wanted to compare the user of the payload data from the jwt-token to the required role. I can't explain how this is possible, but it is only possible to get the user and role out of the payload data, if i let the function return always true. After I set a condition the user is undefined. How is this even possible?

Here is my (shortened) user controller, where I use the Decorator:

    @UseGuards(JwtAuthGuard, RolesGuard)
@Controller('users')
export class UsersController {
  constructor(private readonly userService: UserService) {}
  private readonly logger = new Logger(LoggingService.name);

  @Post('/create')
  async create(@Body() createUserDto: CreateUserDto) {
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
    createUserDto.password = hashedPassword
    return this.userService.createUser(createUserDto);
  }

  @Roles(Role.SUPERADMIN)
  @Get('/')
  showUsers() {
    return this.userService.getUsers();
  }

And here is the roles decorator, with condition:

import { CanActivate, ExecutionContext, Injectable, Logger } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { Observable } from 'rxjs';
import { LoggingService } from 'src/services/logging/logging.service';
import { User } from 'src/user/schemas/user.schema';
import { Role } from '../role.enum';
import { ROLES_KEY } from '../decorators/roles.decorator';
import { Types } from 'mongoose';

@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private readonly reflector: Reflector) {}
  private readonly logger = new Logger(LoggingService.name);

    canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {

        const roles = this.reflector.get<Role[]>(ROLES_KEY, context.getHandler());
        // If there is no Roles-Decorator, just pass through
        if (!roles) {
          return true;
        }

        this.logger.debug("REQUIRED ROLES: ", roles)
        

        const request = context.switchToHttp().getRequest();
        const userRole = request.user?.roles;
        const userID = request.user?.sub;
        this.logger.debug("ROLES GUARD USER", userID);
        this.logger.debug("USER ROLE", userRole);


        // Else, check the request header if it matches

        if (roles.includes(userRole)) {
            return true;
        } else { return false }

  }

}

Logging output (when I try to access the route):

[Nest] 4460  - 25.11.2021, 18:56:33   DEBUG [LoggingService] REQUIRED ROLES: 
[Nest] 4460  - 25.11.2021, 18:56:33   DEBUG [LoggingService] superadmin
[Nest] 4460  - 25.11.2021, 18:56:33   DEBUG [LoggingService] ROLES GUARD USER
[Nest] 4460  - 25.11.2021, 18:56:33   DEBUG [LoggingService] undefined
[Nest] 4460  - 25.11.2021, 18:56:33   DEBUG [LoggingService] USER ROLE
[Nest] 4460  - 25.11.2021, 18:56:33   DEBUG [LoggingService] undefined

But when I do this:

// Else, check the request header if it matches

        // if (roles.includes(userRole)) {
        //     return true;
        // } else { return false }

        return true;

The logging output is this:

[Nest] 39888  - 25.11.2021, 19:00:14   DEBUG [LoggingService] REQUIRED ROLES: 
[Nest] 39888  - 25.11.2021, 19:00:14   DEBUG [LoggingService] superadmin
[Nest] 39888  - 25.11.2021, 19:00:14   DEBUG [LoggingService] ROLES GUARD USER
[Nest] 39888  - 25.11.2021, 19:00:14   DEBUG [LoggingService] 619962ad86e412dc06983a0e
[Nest] 39888  - 25.11.2021, 19:00:14   DEBUG [LoggingService] USER ROLE
[Nest] 39888  - 25.11.2021, 19:00:14   DEBUG [LoggingService] superadmin

The control-flow is top to bottom, right? Any help would be greatly appreciated!

CodePudding user response:

Using APP_GUARD in any module that is registered by the application registers the guard as a global guard, that will run before every request. What I would do is register two global guards, one for the JwtAuthGuard one for the RolesGuard, and implement a @JwtSkip() decorator, that tells the JwtAuthGuard that it can skip authentication on this route (would be the same for the RolesGuard as it shouldn't have any roles associated with it

CodePudding user response:

I found the Solution!

I had not registered the RolesGuard in a global context. So I went ahead and put

app.useGlobalGuards(new RolesGuard(new Reflector()));

in the main.ts. It then suddenly worked. And I modified the condition to pass the Guard as follows:

// Else, check the request header if it matches

        if (roles.some(r => r == userRole)) {
            this.logger.log("ROLES GUARD: PASS")
            return true;
        } else { 
          this.logger.error("ROLES GUARD: DENIED")
          return false 
        }

Hope this helps anyone!

  • Related