Claudio Anibal Morales Pérez
nest g gu auth/guards/jwt-auth --flat
Luis Abdel Rangel Castro
nest generate guard auth/guards/jwt-auth
Andres Trujillo
ty!
¿Cómo proteger los endpoints de un controlador con un JSON Web Token?
Proteger los endpoints de un controlador mediante un JSON Web Token (JWT) es una práctica esencial para garantizar la seguridad de una aplicación eCommerce o cualquier API. Los procesos de obtener, crear, actualizar y eliminar productos deben estar resguardados bajo un sistema de autorización que verifique la autenticidad y permisos del usuario.
NestJS facilita este proceso con la ayuda del paquete "passport" que permite implementar estrategias de autenticación, como JWT. Sin embargo, pese a que la mayoría de los endpoints deben estar protegidos, hay ocasiones, como en el caso del endpoint para obtener productos, que necesitamos que sea accesible públicamente.
Configuración de un guardián personalizado
Para implementar esta funcionalidad, creamos un guardián (también conocido como "guard") a través de NestJS que verifica JWT. Aquí explicamos cómo personalizarlo:
Creación del guardián: Iniciar con el siguiente comando en la terminal del proyecto NestJS:
nest generate guard auth/jwt-auth
Esto generará un archivo base para nuestro nuevo guardián.
Extender las funcionalidades de JWT: En lugar de reescribir la función canActivate, podemos expandirla.
import { Injectable, ExecutionContext } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { canActivate(context: ExecutionContext) { return super.canActivate(context); // Mantiene la funcionalidad original } }
Sin embargo, necesitamos verificar si el acceso debe ser público.
Lectura de metadata: Utilizamos el decorador @Public() para aquellos endpoints que deben ser accesibles sin autenticación.
// Incorporando Reflector para manejar metadata import { Reflector } from '@nestjs/core'; constructor(private reflector: Reflector) {} canActivate(context: ExecutionContext) { const isPublic = this.reflector.getAllAndOverride<boolean>('isPublic', [ context.getHandler(), context.getClass(), ]); if (isPublic) { return true; // Si el endpoint es público, permitir acceso } return super.canActivate(context); // De lo contrario, validar el JWT }
Implementación del decorador @Public
Para que un endpoint sea accesible sin necesidad de un token, creamos un decorador personalizado:
import { SetMetadata } from '@nestjs/common'; export const IS_PUBLIC_KEY = 'isPublic'; export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
Esto permite anotar métodos específicos del controlador para indicar que no requieren autenticación, como el siguiente ejemplo:
@ApiTags('products') @Controller('products') export class ProductsController { @Public() @Get() findAll(): string { return 'This action returns all products'; } }
Prueba del entorno
Con la implementación de este guardián personalizado, puedes verificar su efecto directamente en Insomnia o Postman. Intenta realizar una solicitud GET a tu endpoint público para obtener productos; debe ser accesible sin necesidad de autenticarlo con un token.
Ventajas y consejos prácticos
Flexibilidad: Esta aproximación permite seleccionar qué endpoints son públicos o privados mediante decoradores sencillos, manteniendo un código limpio y organizado.
Reutilización: Esta funcionalidad puede ser aplicable a otros proyectos gracias a la arquitectura modular de NestJS.
Seguridad: Asegúrate de usar tokens seguros y de realizar revisiones periódicas sobre los permisos de acceso de cada endpoint.
Mantener una API segura y eficiente es crucial para proteger los datos de usuario y la integridad de la aplicación. Estos métodos garantizan que los usuarios sólo accedan a los recursos para los cuales están autorizados, al mismo tiempo que permite un acceso público controlado cuando sea necesario. ¡Continúa explorando y aplicando estas prácticas en tus proyectos para sacar el máximo provecho de NestJS!
Claudio Anibal Morales Pérez
nest g gu auth/guards/jwt-auth --flat
Luis Abdel Rangel Castro
nest generate guard auth/guards/jwt-auth
Andres Trujillo
ty!
Rony Porraz Vargas
Andres Trujillo
Ty!
yerson lasso
Jum, sería buena practica cambiar el nombre de "IS_PUBLIC_KEY" a algo como "IS_PUBLIC"? teniendo en consideración que ya no se esta usando un apikey
Nicolas Molina
ProfesorTienes razón los nombres de las variables deben ser muy claras y ya no usamos la KEY pues no debería estar en el nombre.
Naldo Duran
👏
Andrés Esteban Rodríguez Jiménez
Mauricio Jourdan
Gracias por tus apuntes. Ojo, que hay un tipo en canActive. Saludos
Jorge Andres Sanabria Camacho
Andres Trujillo
interesante, gracias!
Nicolas Javier Espinoza Dias
profe, y no es mas facil colocar el @UseGuards(AuthGuard('jwt')) en el endpoint que quiera proteger? en vez de todo el controlador? o es mala practica?
Juan David Merchán Torres
Hola 👋 Claro que puedes hacerlo, pero es mas engorroso y es mas codigo repetido, para cada endpoint que quieres proteger, usualmente son mas los que quieres proteger que los que son publicos asi que es bueno conocer esta implementacion para mejorar la calidad de nuestro codigo.
Espero haberte ayudado 🚀
Brayhan Andres Jaramillo Castaño
Tuve el siguiente erorr:
ERROR [ExceptionsHandler] Unknown authentication strategy "jwt"
Logre solucionarlo de la siguiente manera En la carpeta strategies modificar el archivo en la siguiente línea
export class LocalStrategy extends PassportStrategy(Strategy, 'jwt') {
Andres Trujillo
También es necesario cambiar el nombre de LocalStrategy a JwtStrategy para no tener dos clases repetidas, la otra clase esta en local.strategy
Catalano Nicolas
Una forma mas bonita, o por lo menos para mi, del metodo canActive
canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { return ( this.reflector.get(IS_PUBLIC_KEY, context.getHandler()) || super.canActivate(context) ); }
Rubén Maier Enzler
En el JwtStragey existe alguna forma de acceder a los headers?
Juan David Merchán Torres
Hola 👋 Puedes hacerlo con
... constructor( @Inject(config.KEY) private readonly configService: ConfigType<typeof config>, ) { super({ jwtFromRequest: ExtractJwt.fromHeader('token'), // 🗨️ Donde dice [token] puedes poner el nombre del header que quieres obtener ignoreExpiration: false, secretOrKey: configService.jwt.secret, }); } ...
Espero haberte ayudado 🚀
Asi quedo el guardian
import { ExecutionContext, HttpException, HttpStatus, Injectable, UnauthorizedException } from '@nestjs/common'; import { Reflector } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { constructor( private reflector: Reflector ){ super() } canActivate(context: ExecutionContext) { const isPublic = this.reflector.get('isPublic', context.getHandler()) if( isPublic ) return true; return super.canActivate(context); } handleRequest(err, user, info) { if (err || !user) { throw new HttpException({ status: HttpStatus.UNAUTHORIZED, error: 'Usuario no autorizado', }, HttpStatus.UNAUTHORIZED); } return user; } }
Extendiendo JWT Guard
Ya tenemos la funcionalidad del proteger los distintos Endpoints de nuestra aplicación en base a una estrategia de autenticación con JWT5, ahora, vamos a extender estas funcionalidades, por ejemplo, esta bien que proteja la parte de crear, actualizar y borrar un producto en base a un usuario administrador, pero deberíamos poder ver los productos independientemente de que tengamos una sesión o no.
Vamos a ver como realizar esta extensión:
Para esto, vamos a crear un nuevo guardián.
nest g gu auth/guards/jwt-auth --flat
Ahora vamos a empezar a crear la lógica de como queremos que actúe este guardian:
import { Injectable, ExecutionContext } from '@nestjs/common'; // importamos a reflector para obtener la metadata import { Reflector } from '@nestjs/core'; // importamos a "UseGuards" y a "AuthGuard" import { AuthGuard } from '@nestjs/passport'; // traemos la definición del decorador @IsPublic() que habíamos creado import { IS_PUBLIC_KEY } from '../decorators/public.decorator'; // vamos a extender la funcionalidad del AuthGuard('jwt') @Injectable() export class JwtAuthGuard extends AuthGuard('jwt') { // inyectamos al reflector como una dependencia constructor(private reflector: Reflector) { // como hacemos herencia, llamamos a "super()" super(); } // creamos una funcionalidad para validar la activación // le enviamos el contexto de ejecución donde recibimos lametadata canActive(context: ExecutionContext) { // validamos si el Endpoint tiene en su metadata si es público const IsPublic = this.reflector.get(IS_PUBLIC_KEY, context.getHandler()); // el Endpoint es público? if (IsPublic) { // retornamos el acceso sin necesidad de validar return true; } // si no // le decimos que realize la validación que hace desde su herencia return super.canActivate(context); } }
Bien, ahora lo que vamos a hacer es decirle al product.controller que aplique el guardian que acabamos de crear:
**src/products/controllers/product.controller.ts**:import { UseGuards } from '@nestjs/common'; // importamos el guardian import { JwtAuthGuard } from 'src/auth/guards/jwt-auth.guard'; // traemos al decodador personalizado que hace un Endpoint público import { Public } from 'src/auth/decorators/public.decorator'; // le decimos al controler que en vez, utilice nuestro guardian @UseGuards(JwtAuthGuard) @ApiTags('products') @Controller('products') export class ProductsController { constructor(private productsService: ProductsService) {} // lo usamos en los Endpoints que deseamos hacer públicos @Public() @Get() @ApiOperation({ summary: 'List of products' }) getProducts(@Query() param: FilterProductsDto) { return this.productsService.findAll(param); } // este tambíen @Public() @Get(':productId') @HttpCode(HttpStatus.ACCEPTED) getOne(@Param('productId', MongoIdPipe) productId: string) { return this.productsService.findOne(productId); } // ... }
Y listo, de esta forma estamos utilizando nuestro guardián personalizo que nos permite programar excepciones, así que ya logramos extender el controlador que ya teníamos y logramos tener una lógica reutilizable.
Para proteger todas las rutas por defecto en el api, se puede crear un provider a partir del JwtGuard y aplicarlo en el app.module. De esta manera no habría necesidad de usar repetidamente el @UseGuards(...) y solamente se tendría que usar el decorator @Public() cuando se quiera hacer alguna ruta pública. De forma que el provider quedaría de la siguiente manera:
const providerJwtAuthGuard: Provider<JwtAuthGuard> = { provide: APP_GUARD, useClass: JwtAuthGuard };
Ahora, el provider providerJwtAuthGuard se debe agregar en el arreglo de providers del app.module, quedando de la siguiente manera:
@Module({ imports: [ ... ], controllers: [ ... ], providers: [..., providerJwtAuthGuard, ...], //** }) export class AppModule {}
Con esto, NestJS protegerá todas las rutas y controladores de forma predeterminada, por lo que los endpoint o métodos, que tienen que ser públicos, se les debe aplicar el decorator @Public() de forma explícita, por ejemplo para la ruta de auth/login, permitiendo hacer el logueo sin tener un token, para lo cual no aplica en dicha ruta. De forma que:
@Public() //** @UseGuards(LocalAuthGuard) @Post('/login') async login(@Request() req): Promise<JWTResponseModel> { return this.authService.generateToken(req.user); }
Ahora, para que el JWTGuard soporte el decorator @Public() a nivel de controlador, se debe ajustar el funcionamiento del reflector de tal forma que reconozca el contexto del controlador o de los manejadores de las rutas. El método canActivate podría refactorizarse de manera que:
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> { const isPublic: boolean = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC, [ context.getHandler(), //Contexto de los manejadores de las rutas context.getClass() //Contexto de los controladores ]); if(isPublic) return true; return super.canActivate(context); }
Para más referencias, se puede consultar en la documentación oficial de NestJS, para el tema de autenticación global. Enable authentication globally