Skip to content

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

FixDevs ·

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:

  • 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.
  • 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.

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

For related NestJS issues, see Fix: NestJS Circular Dependency and Fix: NestJS TypeORM QueryFailedError.

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