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¶
- Middleware runs
- Global guards (if any)
- Controller-level guards
- Method-level guards
- Interceptors (pre-handler)
- Pipes
- Route handler
- Interceptors (post-handler)
- 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