Skip to content

nest_07_guards - Guards & Authorization

Guards decide who gets through Not can they reach this route (that's middleware) but does this specific request have permission to access this specific resource If your auth checks live in controller methods instead of guards , you're repeating yourself and that repetition is where auth bypasses hide

what's in here

  • CanActivate interface and execution context
  • Building an auth guard (JWT verification)
  • Role-based access control with custom metadata
  • Global guards , controller guards , route guards
  • Guard execution order and lifecycle integration

the CanActivate interface

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Observable } from 'rxjs'

@Injectable()
export class ExampleGuard implements CanActivate {
  canActivate(
    context: ExecutionContext
  ): boolean | Promise<boolean> | Observable<boolean> {
    return true  // let everything through (pointless but valid)
  }
}

canActivate returns a boolean (or Promise/Observable of boolean) true - let the request through false - Nest throws a ForbiddenException (403) automatically

The ExecutionContext gives you access to the incoming request , response , and handler metadata

building a JWT auth guard

import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common'
import { Request } from 'express'
import { JwtService } from '@nestjs/jwt'

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest()
    const token = this.extractTokenFromHeader(request)

    if (!token) {
      throw new UnauthorizedException('no token provided')
    }

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET
      })
      // attach user to request for downstream use
      request['user'] = payload
    } catch {
      throw new UnauthorizedException('invalid or expired token')
    }

    return true
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? []
    return type === 'Bearer' ? token : undefined
  }
}

The guard extracts the token from the Authorization header Verifies it with jwtService.verifyAsync() Throws UnauthorizedException (returns 401) if the token is missing , invalid , or expired Sets request.user with the decoded payload - controllers access it via @Req() or a custom decorator

applying guards

import { Controller, Get, Post, UseGuards, SetMetadata } from '@nestjs/common'
import { JwtAuthGuard } from './guards/jwt-auth.guard'
import { RolesGuard } from './guards/roles.guard'

@Controller('users')
@UseGuards(JwtAuthGuard)  // controller-level - every route needs auth
export class UsersController {

  @Get('profile')
  getProfile() {
    // JwtAuthGuard applies here
    return 'profile'
  }

  @Get('public')
  @UseGuards()  // overriding - no auth needed for this specific route
  getPublic() {
    return 'public info'
  }

  @Post()
  @UseGuards(RolesGuard)  // multiple guards stack
  @SetMetadata('roles', ['admin'])
  createUser() {
    return 'created'
  }
}

Guards can apply at controller level (all routes) or method level (specific routes) Multiple guards stack - all must return true for the request to proceed @UseGuards() with no arguments removes all guards for that method

role-based access with Reflector

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { Reflector } from '@nestjs/core'

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

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
      context.getHandler(),
      context.getClass()
    ])

    if (!requiredRoles) {
      return true  // no roles required = public
    }

    const { user } = context.switchToHttp().getRequest()
    return requiredRoles.some(role => user.roles?.includes(role))
  }
}

Reflector reads metadata set by @SetMetadata('roles', ['admin']) The guard checks if the authenticated user has any of the required roles If no roles metadata is set on the handler , the guard passes - you only lock down endpoints that explicitly request it

custom role decorator

import { SetMetadata } from '@nestjs/common'

export const Roles = (...roles: string[]) => SetMetadata('roles', roles)

// usage
@Post('admin')
@Roles('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
adminAction() {
  return 'admin only'
}

Cleaner than raw @SetMetadata() The @Roles('admin') decorator sets the metadata , JwtAuthGuard authenticates , RolesGuard authorizes

global guards

Register a guard globally via APP_GUARD token:

import { Module } from '@nestjs/common'
import { APP_GUARD } from '@nestjs/core'
import { JwtAuthGuard } from './guards/jwt-auth.guard'

@Module({
  providers: [
    {
      provide: APP_GUARD,
      useClass: JwtAuthGuard
    }
  ]
})
export class CoreModule {}

Global guards run on every route in every module Public endpoints need their own metadata or explicit @UseGuards() override to bypass

@Controller('health')
export class HealthController {
  @Get()
  @SkipAuth()  // custom decorator that marks this route as public
  check() {
    return { status: 'ok' }
  }
}
// public decorator
import { SetMetadata } from '@nestjs/common'
export const IS_PUBLIC_KEY = 'isPublic'
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true)

// updated guard with bypass
@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass()
    ])

    if (isPublic) return true  // skip auth for public routes

    // ... rest of auth logic
  }
}

guard execution order

  1. Middleware runs
  2. Global guards (if any)
  3. Controller-level guards
  4. Method-level guards
  5. Interceptors (pre-handler)
  6. Pipes
  7. Route handler
  8. Interceptors (post-handler)
  9. Exception filters (if error)

If any guard returns false or throws , the request stops there This is why guards for auth should run early - reject unauthorized before any processing happens

security pattern: audit logging in guards

@Injectable()
export class AuditGuard implements CanActivate {
  constructor(
    private readonly auditService: AuditService
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    if (process.env.NODE_ENV === 'production') {
      const request = context.switchToHttp().getRequest()
      await this.auditService.logAccess({
        userId: request.user?.id,
        path: request.originalUrl,
        method: request.method,
        ip: request.ip,
        timestamp: new Date().toISOString(),
        userAgent: request.headers['user-agent']
      })
    }
    return true  // always pass , just log
  }
}

An audit guard logs every request without affecting the response Useful for compliance (PCI-DSS , HIPAA require access logging)

prerequisites

nest_06_middleware - Middleware


next -> nest_08_interceptors - Interceptors