Fix: NestJS Guard Not Working — canActivate Always Passes or Is Never Called
Quick Answer
How to fix NestJS guards not working — applying guards globally vs controller vs method level, JWT AuthGuard, metadata with Reflector, public routes, and guard execution order.
The Problem
A NestJS guard is registered but never blocks access:
@UseGuards(JwtAuthGuard)
@Get('protected')
getProtectedData() {
return { secret: 'data' };
}
// Returns data even without a valid JWT token — guard is not blockingOr the guard is applied but canActivate returns true even though the user shouldn’t have access:
@Injectable()
export class RolesGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
console.log('Required roles:', requiredRoles); // logs undefined
// requiredRoles is undefined → no check performed → returns true
return true;
}
}Or a globally applied guard blocks routes that should be public:
POST /auth/login → 401 Unauthorized
// Login route is being blocked by the global auth guardWhy This Happens
NestJS guards run in a specific order and scope — misconfiguration at any level causes the guard to be ineffective or overly restrictive:
- Guard not applied at the right level — NestJS guards can be applied globally, per-controller, or per-method. If applied at the wrong level, they don’t run for the intended routes.
@UseGuards()applied to the wrong target — decorators must be applied directly to the class or method they protect. A guard on a parent class doesn’t automatically protect child class methods.Reflectorreading metadata from wrong target —Reflector.get()andReflector.getAllAndOverride()require the correct handler or class target. Using the wrong context method (getHandler()vsgetClass()) returnsundefined.- Missing
@SetMetadata()/ custom decorator — role-based guards rely on metadata set by decorators. If the metadata decorator isn’t applied to the route,Reflector.get()returnsundefinedand the guard defaults to allowing access. - Global guard blocks all routes including public ones — when registered globally via
APP_GUARD, the guard applies to every route including login, health checks, and public endpoints. - Guard class not decorated with
@Injectable()— without@Injectable(), NestJS can’t create the guard as a provider, and it may silently fail.
Fix 1: Apply the Guard at the Correct Level
NestJS guards can be applied at three levels — each with different scope:
// Method level — protects only this specific endpoint
@Get('protected')
@UseGuards(JwtAuthGuard)
getProtectedData() {
return { secret: 'data' };
}
// Controller level — protects ALL endpoints in this controller
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
getUsers() { ... } // Protected
@Get('stats')
getStats() { ... } // Also protected
}
// Global level — protects every route in the application
// In AppModule:
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],@UseGuards() decoration order matters — multiple guards run left to right. If the first guard blocks access, subsequent guards don’t run:
// JwtAuthGuard runs first, then RolesGuard
@UseGuards(JwtAuthGuard, RolesGuard)
@Get('admin')
adminRoute() { ... }Fix 2: Implement JwtAuthGuard Correctly
The most common guard — JWT authentication — requires Passport integration:
npm install @nestjs/passport passport passport-jwt @nestjs/jwt
npm install --save-dev @types/passport-jwt// auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
async validate(payload: { sub: number; email: string }) {
// Return value is attached to request.user
return { userId: payload.sub, email: payload.email };
}
}// auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
// AuthGuard('jwt') uses the JwtStrategy automatically
// No need to override canActivate for basic JWT validation
}// auth/auth.module.ts
@Module({
imports: [
PassportModule,
JwtModule.registerAsync({
useFactory: (config: ConfigService) => ({
secret: config.get('JWT_SECRET'),
signOptions: { expiresIn: '7d' },
}),
inject: [ConfigService],
}),
],
providers: [JwtStrategy, JwtAuthGuard],
exports: [JwtAuthGuard], // Export so other modules can use it
})
export class AuthModule {}Fix 3: Fix Reflector Usage in Role-Based Guards
Reflector reads metadata set by decorators. Incorrect usage is the most common cause of role guards not working:
// Custom decorator — sets metadata on the route
// auth/decorators/roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);// auth/guards/roles.guard.ts
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 {
// WRONG — reads only from the method handler (misses class-level decorators)
const roles = this.reflector.get<string[]>('roles', context.getHandler());
// CORRECT — reads from method first, falls back to class
const roles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(), // Method-level decorator
context.getClass(), // Controller-level decorator
]);
if (!roles || roles.length === 0) {
return true; // No roles required — allow access
}
const request = context.switchToHttp().getRequest();
const user = request.user; // Set by JwtAuthGuard (runs before RolesGuard)
if (!user) return false;
return roles.some(role => user.roles?.includes(role));
}
}Apply the decorator to routes:
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {
@Get('users')
@Roles('admin', 'superuser') // ← Metadata set here
getUsers() {
return this.usersService.findAll();
}
@Delete('users/:id')
@Roles('superuser') // ← Only superuser can delete
deleteUser(@Param('id') id: string) {
return this.usersService.remove(id);
}
}Fix 4: Allow Public Routes When Using Global Guards
When JwtAuthGuard is applied globally, public routes (login, registration, health checks) also get blocked. Use a @Public() decorator to skip the guard:
// auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);// auth/guards/jwt-auth.guard.ts — override canActivate to check for @Public()
import { Injectable, ExecutionContext } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(private reflector: Reflector) {
super();
}
canActivate(context: ExecutionContext) {
// Check if this route is marked as public
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) {
return true; // Skip JWT validation for public routes
}
// Otherwise, run standard JWT validation
return super.canActivate(context);
}
}// app.module.ts — register globally
import { APP_GUARD } from '@nestjs/core';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
],
})
export class AppModule {}// auth/auth.controller.ts — mark public routes
@Controller('auth')
export class AuthController {
@Public() // ← Skip global JwtAuthGuard for login
@Post('login')
login(@Body() loginDto: LoginDto) {
return this.authService.login(loginDto);
}
@Public() // ← Registration is also public
@Post('register')
register(@Body() registerDto: RegisterDto) {
return this.authService.register(registerDto);
}
}
// Health check controller
@Controller('health')
export class HealthController {
@Public()
@Get()
check() {
return { status: 'ok' };
}
}Fix 5: Debug Guard Execution with Logging
Add logging to understand why a guard is or isn’t running:
@Injectable()
export class DebugGuard implements CanActivate {
private readonly logger = new Logger('DebugGuard');
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const handler = context.getHandler();
const controller = context.getClass();
this.logger.log(`Guard called for ${controller.name}.${handler.name}`);
this.logger.log(`Request path: ${request.path}`);
this.logger.log(`User: ${JSON.stringify(request.user)}`);
this.logger.log(`Headers: ${JSON.stringify(request.headers.authorization)}`);
return true; // Temporarily allow all — for debugging only
}
}Check the guard execution order in Passport guards:
AuthGuard from @nestjs/passport provides hooks for debugging:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
handleRequest(err: Error, user: any, info: any, context: ExecutionContext) {
// info contains the JWT error if validation fails
if (info) {
console.log('JWT validation info:', info.message);
// 'JsonWebTokenError: invalid signature'
// 'TokenExpiredError: jwt expired'
// 'No auth token' — Authorization header missing
}
if (err || !user) {
throw err || new UnauthorizedException(info?.message);
}
return user; // Attach to request.user
}
}Fix 6: Fix Guard Issues in WebSocket and GraphQL Contexts
Guards work differently outside HTTP context — the ExecutionContext type changes:
WebSocket guards:
@Injectable()
export class WsAuthGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
// WebSocket context — use switchToWs() instead of switchToHttp()
const client = context.switchToWs().getClient();
const data = context.switchToWs().getData();
const token = client.handshake?.auth?.token;
if (!token) return false;
try {
const payload = this.jwtService.verify(token);
client.user = payload; // Attach user to WebSocket client
return true;
} catch {
return false;
}
}
}GraphQL guards:
@Injectable()
export class GqlAuthGuard extends AuthGuard('jwt') {
// Override getRequest to extract request from GraphQL context
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
return ctx.getContext().req; // GraphQL context wraps the HTTP request
}
}// GraphQL resolver — apply the guard
@Resolver(() => User)
export class UsersResolver {
@UseGuards(GqlAuthGuard)
@Query(() => User)
me(@CurrentUser() user: User) {
return this.usersService.findOne(user.id);
}
}Fix 7: Common Configuration Mistakes
Guard not exported from its module:
// auth.module.ts — must export guards for use in other modules
@Module({
providers: [JwtAuthGuard, RolesGuard, JwtStrategy],
exports: [
JwtAuthGuard, // ← Without this, other modules can't inject or use the guard
RolesGuard,
],
})
export class AuthModule {}Missing AuthModule import in the module using the guard:
// users.module.ts — must import AuthModule to use its guards
@Module({
imports: [
AuthModule, // ← Import the module that provides JwtAuthGuard
],
controllers: [UsersController],
providers: [UsersService],
})
export class UsersModule {}@UseGuards() applied in the wrong order — AuthGuard (authentication) must run before role/permission guards (authorization):
// CORRECT order — authenticate first, authorize second
@UseGuards(JwtAuthGuard, RolesGuard)
// WRONG — RolesGuard runs before user is authenticated
// RolesGuard reads request.user, which isn't set yet
@UseGuards(RolesGuard, JwtAuthGuard)Still Not Working?
Check if canActivate is even being called — add a console.log as the very first line of canActivate. If it doesn’t print, the guard is not being invoked. This usually means the guard isn’t properly registered.
Verify @Injectable() is on the guard class — without it, NestJS can’t inject dependencies like Reflector or JwtService, and the guard may fail silently.
Exception filters may be swallowing guard errors — if a custom exception filter catches all errors including UnauthorizedException, it may return a generic 500 instead of 401. Check the filter:
@Catch()
export class AllExceptionsFilter implements ExceptionFilter {
catch(exception: unknown, host: ArgumentsHost) {
// Make sure to handle HttpException correctly
if (exception instanceof HttpException) {
const status = exception.getStatus();
// Re-throw or handle appropriately
}
}
}For related NestJS issues, see Fix: NestJS Circular Dependency and Fix: NestJS TypeORM QueryFailedError.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: NestJS Swagger UI Not Showing — /api-docs Returns 404 or Blank Page
How to fix NestJS Swagger UI not displaying — SwaggerModule setup, DocumentBuilder, decorators not appearing, guards blocking the docs route, and Fastify vs Express differences.
Fix: NestJS Nest can't resolve dependencies — Provider Not Found Error
How to fix NestJS dependency injection errors — module imports, provider exports, circular dependencies, dynamic modules, and the most common 'can't resolve dependencies' patterns.
Fix: NestJS ValidationPipe Not Working — class-validator Decorators Ignored
How to fix NestJS ValidationPipe not validating requests — global pipe setup, class-transformer, whitelist and transform options, custom validators, and DTO inheritance issues.
Fix: NestJS Interceptor Not Triggered — Interceptors Not Running
How to fix NestJS interceptors not being called — global vs controller vs method binding, response transformation, async interceptors, execution context, and interceptor ordering.