This chapter follows Authentication in NestJS.
Authentication put a verified user on request.user. Authorization uses it to answer two different questions. The first is about the route: may a user with these roles call DELETE /cats/:id at all? A guard answers that before the handler runs, from metadata on the route. The second is about the data: may this user delete this cat? Only code that has loaded the cat can answer that, so it belongs in the service. Most authorization bugs come from answering only the first question.
Roles as metadata
Roles are a fixed list, and the routes declare which ones they need. Reflector.createDecorator() gives a typed decorator for that:
import { Reflector } from '@nestjs/core';
export enum Role { User = 'user', Admin = 'admin',}
export const Roles = Reflector.createDecorator<Role[]>();@Controller('cats')export class CatsController { @Get() findAll() {}
@Delete(':id') @Roles([Role.Admin]) remove(@Param('id') id: string) {}}The decorator only stores data. Nothing is enforced until a guard reads it.
A roles guard
The guard reads the required roles from the handler, falling back to the controller, and compares them with the roles on the authenticated user:
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';import { Reflector } from '@nestjs/core';import type { AuthUser } from './auth-user';import { Roles } from './roles';
@Injectable()export class RolesGuard implements CanActivate { constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean { const required = this.reflector.getAllAndOverride(Roles, [context.getHandler(), context.getClass()]); if (!required?.length) return true;
const { user } = context.switchToHttp().getRequest<{ user?: AuthUser }>(); return required.some((role) => user?.roles.includes(role)); }}A route without @Roles() is open to every authenticated user. Returning false makes Nest answer 403 Forbidden. That is the correct status here: the user is known but not allowed, which is different from the 401 that authentication sends for an unknown user.
Running guards in the right order
RolesGuard reads request.user, so it must run after the JWT guard has set it. Global guards run in the order they are registered. Registering both in the same module, in this order, makes the dependency explicit:
providers: [ AuthService, LocalStrategy, JwtStrategy, { provide: APP_GUARD, useClass: JwtAuthGuard }, { provide: APP_GUARD, useClass: RolesGuard },],Roles copied into the token at sign-in are a snapshot. Revoking a user’s admin role takes effect only when their token expires, unless JwtStrategy.validate() reloads the user on every request. Short token lifetimes keep that window small.
The current user as a parameter
Handlers that need the user should not reach for @Req() and the platform’s request type. A custom parameter decorator returns just the user:
import { createParamDecorator, ExecutionContext } from '@nestjs/common';import type { AuthUser } from './auth-user';
export const CurrentUser = createParamDecorator( (_data: unknown, context: ExecutionContext): AuthUser => context.switchToHttp().getRequest<{ user: AuthUser }>().user,);@Delete(':id')@HttpCode(204)remove(@Param('id') id: string, @CurrentUser() user: AuthUser) { return this.catsService.remove(id, user);}The controller now passes the user on as data, and the service does not depend on HTTP.
Checking ownership in the service
A guard cannot see which cat :id refers to without loading it, and loading data in a guard duplicates the service. The service already loads the cat, so it checks ownership there:
async remove(id: string, user: AuthUser): Promise<void> { const cat = await this.findOne(id); const isOwner = String(cat.owner) === user.id; if (!isOwner && !user.roles.includes(Role.Admin)) { throw new ForbiddenException('You can only delete your own cats'); } await this.catModel.deleteOne({ _id: id }).exec();}The same applies to lists: findAll() for a normal user filters by owner in the query, rather than loading everything and filtering afterwards. Forgetting this check is the classic insecure direct object reference, where any signed-in user can read or change another user’s data by guessing an ID.
Beyond roles
Roles stay manageable while the rules are “admins may do everything, users may do some things”. When rules start to combine roles, ownership, and resource state (“editors may publish articles in their own team that are not archived”), a policy library keeps them in one place. @casl/ability is the common choice in Nest projects. An AbilityFactory provider builds the user’s permissions, and both a guard and the services ask it ability.can('update', cat). The two-level split stays the same: coarse checks in a guard from route metadata, fine checks wherever the resource is loaded.
Next: Testing NestJS applications.
Check your understanding
Question 1 of 6
0 of 6 checked