Foundations View as Markdown

Guards, authentication and authorisation

Guards answer one question: may this request proceed? Getting the difference between authentication and authorisation right is where most back-end vulnerabilities live.

A guard runs before the route handler and returns a boolean. false produces a 403; throwing gives you control of the response.

src/auth/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private readonly jwt: JwtService, private readonly reflector: Reflector) {}

  async canActivate(ctx: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC, [
      ctx.getHandler(), ctx.getClass(),
    ]);
    if (isPublic) return true;

    const req = ctx.switchToHttp().getRequest();
    const token = req.headers.authorization?.replace(/^Bearer /, '');
    if (!token) throw new UnauthorizedException();

    try {
      req.user = await this.jwt.verifyAsync(token);
      return true;
    } catch {
      throw new UnauthorizedException();
    }
  }
}

Default deny#

Register the guard globally and opt routes out, never the reverse.

src/app.module.ts
providers: [{ provide: APP_GUARD, useClass: JwtAuthGuard }]
ts
export const IS_PUBLIC = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC, true);

@Public()
@Post('login')
login(@Body() dto: LoginDto) {}

Roles#

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

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

  canActivate(ctx: ExecutionContext): boolean {
    const required = this.reflector.getAllAndOverride<Role[]>('roles', [
      ctx.getHandler(), ctx.getClass(),
    ]);
    if (!required?.length) return true;
    const { user } = ctx.switchToHttp().getRequest();
    return required.some((r) => user?.roles?.includes(r));
  }
}
ts
@Roles(Role.Admin)
@Delete(':id')
remove(@Param('id') id: string) {}

The check that gets forgotten#

Roles answer what kind of user is this. They do not answer is this their resource, and that second question is where the real vulnerabilities are.

ts
@Get(':id')
findOne(@Param('id') id: string) {
  return this.invoices.findOne(id);      // any authenticated user, any invoice
}

Ownership depends on data, so it belongs in the service, where the query happens:

ts
async findOne(id: string, requester: AuthUser): Promise<Invoice> {
  const invoice = await this.repo.findById(id);
  if (!invoice || invoice.ownerId !== requester.id) {
    throw new NotFoundException();       // not Forbidden: do not confirm it exists
  }
  return invoice;
}

Return 404 rather than 403 for resources the caller may not see. A 403 tells an attacker the id is real.

And write the test, once per resource type:

ts
it('does not let a user read another user’s invoice', async () => {
  await request(app.getHttpServer())
    .get(`/invoices/${bobsInvoice.id}`)
    .set('Authorization', aliceToken)
    .expect(404);
});

Guards, interceptors, pipes, filters#

The execution order, which is worth memorising:

middleware -> guards -> interceptors (before) -> pipes -> handler
           -> interceptors (after) -> exception filters
  • Guard — may this proceed? Auth, roles, rate limits.
  • Pipe — transform and validate input. DTO validation.
  • Interceptor — wrap the call. Logging, caching, response shaping, timeouts.
  • Filter — turn a thrown exception into a response.

Putting logic in the wrong one is a common source of surprise: a pipe cannot reject on authorisation grounds cleanly, and a guard cannot see the parsed body.

Common questions#

Guard or middleware?#

Guards know about the route — they get the handler, the class and its metadata, so they can read decorators like @Roles(). Middleware runs before routing and knows only the request. Use guards for anything decision-based.

Where do I check that a user owns a record?#

In the service, where the record is fetched. A guard runs before the handler and would have to query the database itself, duplicating work and drifting out of sync with the real query.

403 or 404 for a resource the user cannot access?#

404, for anything with a guessable or enumerable id. A 403 confirms the resource exists, which is an information leak. Use 403 when the caller is allowed to know it exists but not to act.

Get the NestJS agent pack

A battle-tested AGENTS.md, the review checklist, and the failure-mode cheat sheet for NestJS. One email, then occasional updates when the tooling shifts. No course pitch.

Unsubscribe in one click. We never sell the list. Or just take the AGENTS.md now — no email needed.