En esta página

Guards y autorización basada en roles

14 min lectura TextoCap. 2 — Servicios e inyección

¿Qué son los guards en NestJS?

Los guards son clases que implementan la interfaz CanActivate y tienen una sola responsabilidad: determinar si una petición debe ser procesada o rechazada. Son similares a los middlewares de Express, pero tienen acceso al contexto de ejecución de NestJS, lo que les permite saber qué controlador y método están a punto de ejecutarse.

La diferencia clave entre guards y middlewares es que los guards tienen acceso a la información de metadatos del controlador a través del ExecutionContext y el Reflector. Esto los hace ideales para autorización basada en roles y permisos.

El contrato CanActivate

Todo guard debe implementar el método canActivate:

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

@Injectable()
export class MiGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean> {
    // Retorna true para permitir, false para denegar (lanza 403)
    // O lanza una excepción HTTP específica para más control
    return true;
  }
}

El método puede ser síncrono o asíncrono. Si retorna false, NestJS lanza automáticamente una ForbiddenException (403). Si quieres un código de error diferente (como 401), lanza la excepción explícitamente.

ExecutionContext — El contexto de ejecución

El ExecutionContext es el superconjunto del ArgumentsHost que proporciona información adicional sobre el manejador actual:

canActivate(context: ExecutionContext): boolean {
  // Acceder al request de HTTP
  const request = context.switchToHttp().getRequest<Request>();
  const response = context.switchToHttp().getResponse<Response>();

  // Obtener el nombre del controlador y el método
  const controllerName = context.getClass().name;
  const methodName = context.getHandler().name;

  console.log(`${controllerName}.${methodName} fue llamado`);

  return true;
}

Aplicando guards

Los guards se pueden aplicar en tres niveles:

// Nivel de método
@Get('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
metodoProtegido() {}

// Nivel de controlador
@Controller('admin')
@UseGuards(JwtAuthGuard, RolesGuard)
export class AdminController {}

// Nivel global — en app.module.ts (recomendado)
import { APP_GUARD } from '@nestjs/core';

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

El patrón de guard global + @Public()

La práctica más recomendada es registrar el JwtAuthGuard globalmente y marcar las rutas públicas con un decorador @Public(). Así todas las rutas están protegidas por defecto:

// main.ts o app.module.ts
providers: [
  {
    provide: APP_GUARD,
    useClass: JwtAuthGuard,
  },
]

// Rutas que no requieren autenticación
@Controller('auth')
export class AuthController {

  @Post('login')
  @Public() // ← exento del guard global
  login(@Body() loginDto: LoginDto) {}

  @Post('registro')
  @Public()
  registro(@Body() registroDto: RegistroDto) {}
}

Reflector — Leyendo metadatos de decoradores

El Reflector es el servicio de NestJS que permite leer los metadatos establecidos con SetMetadata. Es la pieza que conecta los decoradores de metadatos con los guards:

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

  canActivate(context: ExecutionContext): boolean {
    // getAllAndOverride busca primero en el handler, luego en la clase
    // El primer valor encontrado tiene prioridad
    const roles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
      context.getHandler(),  // nivel de método
      context.getClass(),    // nivel de controlador
    ]);

    // getAllAndMerge combina los metadatos de handler Y clase
    const todosLosRoles = this.reflector.getAllAndMerge<string[]>(ROLES_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    return true;
  }
}

Decoradores personalizados de parámetros

Además de los decoradores de metadatos, puedes crear decoradores de parámetros que extraen datos del request:

// decorators/ip-cliente.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
import { Request } from 'express';

export const IpCliente = createParamDecorator(
  (_data: unknown, ctx: ExecutionContext): string => {
    const request = ctx.switchToHttp().getRequest<Request>();
    return (
      (request.headers['x-forwarded-for'] as string)?.split(',')[0] ??
      request.socket.remoteAddress ??
      'desconocido'
    );
  },
);

// Uso en controlador
@Post('login')
@Public()
async login(
  @Body() loginDto: LoginDto,
  @IpCliente() ip: string,
) {
  await this.auditoriaService.registrarIntento(loginDto.email, ip);
  return this.authService.login(loginDto);
}

Guard de suscripción activa

Un ejemplo real de guard de negocio —verificar si el usuario tiene una suscripción activa:

@Injectable()
export class SuscripcionActivaGuard implements CanActivate {
  constructor(private readonly suscripcionesService: SuscripcionesService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const { user } = context.switchToHttp().getRequest<RequestWithUser>();

    if (!user) {
      throw new UnauthorizedException();
    }

    const suscripcion = await this.suscripcionesService.findByUsuario(user.sub);

    if (!suscripcion || suscripcion.estado !== 'activa') {
      throw new ForbiddenException(
        'Esta funcionalidad requiere una suscripción activa. ' +
        'Visita /planes para ver las opciones disponibles.'
      );
    }

    return true;
  }
}

Guards en WebSockets y contextos no-HTTP

El ExecutionContext soporta múltiples contextos de transporte. Para guards que funcionen tanto en HTTP como en WebSockets:

@Injectable()
export class UniversalAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const tipo = context.getType();

    if (tipo === 'http') {
      const req = context.switchToHttp().getRequest<Request>();
      return this.validarToken(req.headers.authorization);
    }

    if (tipo === 'ws') {
      const client = context.switchToWs().getClient<WebSocket>();
      const data = context.switchToWs().getData<{ token: string }>();
      return this.validarToken(`Bearer ${data.token}`);
    }

    return false;
  }

  private validarToken(authHeader: string | undefined): boolean {
    if (!authHeader?.startsWith('Bearer ')) return false;
    // lógica de validación del token JWT...
    return true;
  }
}

Los guards son la tercera gran pieza del pipeline de NestJS, después de los pipes y antes de los interceptores. En la próxima lección comenzaremos a trabajar con bases de datos reales usando TypeORM —la ORM más popular del ecosistema NestJS.

Aplica los guards globalmente para mayor seguridad
En lugar de decorar cada ruta con `@UseGuards(JwtAuthGuard)`, registra el guard globalmente en `app.module.ts` como provider con `APP_GUARD`. Esto garantiza que TODAS las rutas estén protegidas por defecto, y usa el decorador `@Public()` para marcar las rutas que no requieren autenticación (login, registro, etc.).
El orden de los guards importa
Cuando usas múltiples guards, se ejecutan en el orden en que se declaran. El `JwtAuthGuard` debe ir ANTES que el `RolesGuard`, porque el guard de roles necesita el payload del JWT que el guard de autenticación adjunta al request. Si el orden es inverso, el guard de roles intentará acceder a `request.user` que aún no existe.