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!
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