Skip to content

Fix: NestJS Guard Not Working — canActivate Always Passes or Is Never Called

FixDevs · (Updated: )

Part of:  JavaScript & TypeScript Errors

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 blocking

Or 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 guard

Why This Happens

NestJS guards run in a specific order and scope — misconfiguration at any level causes the guard to be ineffective or overly restrictive.

The execution pipeline in NestJS follows a strict sequence: middleware runs first, then guards, then interceptors, then pipes, and finally the route handler. Guards sit early in this pipeline, and their purpose is to decide whether a request should proceed. The fact that guards run before interceptors matters because interceptors can modify the request or response — but only if the guard has already allowed the request through.

When you register multiple guards, they execute in a defined order: global guards (registered via APP_GUARD) run first, then controller-level guards, then method-level guards. Within the same level, guards execute left to right as listed in @UseGuards(). If any guard returns false or throws, the pipeline stops. Understanding this ordering is critical because a guard at one level might depend on data set by a guard at a previous level — and if that dependency isn’t met, the guard silently does nothing or returns the wrong result.

Common specific causes:

  • 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.
  • Reflector reading metadata from wrong targetReflector.get() and Reflector.getAllAndOverride() require the correct handler or class target. Using the wrong context method (getHandler() vs getClass()) returns undefined.
  • 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() returns undefined and 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.

Diagnostic Timeline

When a guard isn’t blocking or is blocking too much, resist the urge to immediately re-register it at a different level. Walk through these steps to find the actual failure point.

Minute 0 — Add a console.log to canActivate. Put console.log('GUARD HIT', context.getHandler().name) as the very first line of canActivate. Hit the route. If nothing prints, the guard is not being invoked at all. This tells you the problem is registration, not logic.

Minute 2 — Check APP_GUARD binding. If the guard is global, search your AppModule for APP_GUARD. Verify the useClass points to the correct guard class. A typo or a different class name means NestJS creates the wrong guard. Also verify that the module providing the guard’s dependencies (like JwtModule or ConfigModule) is imported into AppModule.

Minute 4 — Check the return value. If the console.log fires but the guard still lets requests through, log the return value. A guard that returns undefined instead of true or false is treated as falsy in some contexts but may not throw. Explicitly return true or false — never rely on implicit returns.

Minute 6 — Inspect Reflector metadata. For role-based guards, log what this.reflector.getAllAndOverride('roles', [context.getHandler(), context.getClass()]) returns. If it returns undefined, the @Roles() decorator isn’t applied to the route handler or controller. Check the decorator import path — a re-exported decorator from a different barrel file may set a different metadata key.

Minute 8 — Test guard ordering. If you have @UseGuards(JwtAuthGuard, RolesGuard), RolesGuard depends on request.user being set by JwtAuthGuard. If JwtAuthGuard runs but doesn’t attach the user, RolesGuard checks roles against undefined and may default to “allow.” Log request.user inside RolesGuard to confirm it exists.

Minute 10 — Check for exception filters. A custom @Catch() filter that catches all exceptions (including UnauthorizedException) may swallow the 401 response and return a generic 500 or even a 200. Temporarily remove custom exception filters and test again.

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 orderAuthGuard (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
    }
  }
}

Guard works in unit tests but not in e2e tests — e2e tests use a real NestJS app instance. If the test module is created with Test.createTestingModule() but doesn’t include the guard’s module (AuthModule), the guard won’t have its dependencies injected. Add the auth module to the test setup:

const moduleRef = await Test.createTestingModule({
  imports: [AppModule],   // Use the full app module — not a trimmed version
}).compile();

Fastify adapter instead of Express — if your NestJS app uses @nestjs/platform-fastify, context.switchToHttp().getRequest() returns a Fastify request object, not an Express request. Properties like request.user may be set differently. Check the Passport Fastify adapter documentation.

Guard returns a Promise but doesn’t await properlycanActivate can return boolean | Promise<boolean> | Observable<boolean>. If your guard calls an async service (database lookup, external API), make sure it returns the awaited result:

async canActivate(context: ExecutionContext): Promise<boolean> {
  const user = await this.usersService.findById(userId);  // Must await
  return !!user;
}

For related NestJS issues, see Fix: NestJS Circular Dependency, Fix: NestJS Validation Pipe Not Working, Fix: NestJS Module Not Found, and Fix: NestJS Interceptor Not Triggered.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles