En la gestión de aplicaciones, el control de roles es fundamental para garantizar la seguridad y la personalización de la experiencia del usuario. Aquí exploraremos cómo implementar un sistema de roles y permisos utilizando JSON Web Tokens (JWT) y decoradores en aplicaciones TypeScript. Este enfoque permite definir qué operaciones puede realizar un usuario dependiendo de su rol.
¿Cómo empezamos a definir roles?
Lo primero es definir claramente los roles en la aplicación. Para esto, utilizamos las capacidades de TypeScript, como los enumerators o "enums", los cuales son conjuntos de valores predefinidos.
Con estos enums, podemos evitar errores comunes como mal escribir el rol y garantizar que solo los roles predefinidos sean utilizados.
¿Cómo se implementa un decorador para roles?
Después de definir los roles, podemos crear un decorador que nos permita asignar roles específicos a las rutas de nuestra aplicación. Esto se hace añadiendo metadata al endpoint.
Mediante este decorador, podemos definir qué roles tienen acceso a cada endpoint de la siguiente manera:
import{Controller,Post,UseGuards}from'@nestjs/common';import{JwtAuthGuard}from'./guards/jwt-auth.guard';import{RolesGuard}from'./guards/roles.guard';import{Roles}from'./decorators/roles.decorator';import{Role}from'./models/rules.models';@Controller('api')@UseGuards(JwtAuthGuard,RolesGuard)exportclassApiController{@Post('create')@Roles(Role.Admin)create(){// lógica del endpoint}}
¿Cómo funciona el guardián de roles?
El guardián actúa como un guardián de acceso, verificando si el usuario tiene los permisos adecuados para realizar una acción específica. Este proceso incluye verificar si el rol del usuario coincide con el permitido en el decorador.
Para obtener y verificar la metadata, utilizamos el reflección:
Aquí es donde las piezas se unen. Al definir nuestro decorador y guardián de roles, los configuramos en los controladores. Esto permite que solo los usuarios con el rol correcto accedan a determinados endpoints.
Por ejemplo, al utilizar Postman o Insomnia para probar el acceso, podrías realizar una solicitud para crear un producto. Si el usuario es un administrador, la operación se permite; de lo contrario, se deniega.
¿Qué pasa con los endpoints sin roles definidos?
Si hay endpoints que no requieren verificación de roles, simplemente establecemos una condición para dejarlos pasar si no hay metadata de roles:
canActivate(context:ExecutionContext):boolean{const requiredRoles =this.reflector.getAllAndOverride<Role[]>(ROLES_KEY,[ context.getHandler(), context.getClass(),]);if(!requiredRoles){returntrue;}// Procedemos con la lógica de verificación de roles}
El manejo de roles es clave en aplicaciones modernas para mantener la seguridad y personalización de usuario. Si tu aplicación maneja roles dinámicos, podrías ampliar esta lógica integrando consultas a una base de datos para roles y permisos. Siempre es genial ver cómo las diferentes piezas del código trabajan en conjunto para crear soluciones eficientes y robustas, así que sigue perfeccionando tus habilidades y explorando nuevas posibilidades en el mundo del desarrollo.
Control de Roles y Permisos con JSON Web Tokens en TypeScript
403 Forbidden : "El cliente no tiene derechos de acceso al contenido; es decir, no está autorizado, por lo que el servidor se niega a proporcionar el recurso solicitado. A diferencia del 401, el servidor conoce la identidad del cliente."
Tengo una duda de seguridad con respecto a esta solución, en teoría seria posible que un usuario siga teniendo privilegios de un cierto role incluso si se cambia en base de datos porque el backend esta tomando el role del JWT no? y tendría acceso hasta que se expire el toquen
También existe la posibilidad de como dice al final de la clase en lugar de consultar el role en el token consultarlos en la base de datos para que la información sea más actual.
Si el sistema de verificación, solo comprueba si el token es válido y tras pasar la validación usas el rol y devuelves la request, hasta que el token deje de ser válido, el rol seguirá siendo válido para el usuario asociado a ese token.
Los archivos .spec que se generan en el proyecto son para la ejecución de pruebas unitarias o de integración, por lo tanto si no vamos a programar pruebas unitarias, no es necesario tener estos archivos.
Esperemos que generen un curso de pruebas con nestjs para utilizar estos archivos
Igual podríamos validar isPublic y cómo ultimo recurso el array vacío.
Control de roles en NestJS
Ya logramos proteger nuestros Enpoint con una autenticación de usuario, pero un usuario común de nuestra aplicación no puede tener los mismos permisos que tiene un usuario administrador, es por ende que tenemos que crear un control de roles en nuestra aplicación.
Para esto primero tenemos que definir cuales son los roles en nuestra aplicación, para esto vamos a ir a la carpeta de models y crearemos el siguiente modelo:
src/auth/models/roles.model.ts:
// vamos a crear un enum con dos rolesexportenumRole{CUSTOMER='customer',ADMIN='admin',}
Una vez creada esta estructura, vamos a crear un decorador propio que nos permita validar estos roles:
src/auth/decorators/roles.decorator.ts:
// importamos el inyectador de metadataimport{SetMetadata}from'@nestjs/common';// nos traemos al modelo de roleimport{Role}from'../models/roles.model';// creamos una llave indicadoraexportconstROLES_KEY='roles';// creamos un decorador en el que recibiremos un arreglo de rolesexportconstRoles=(...roles:Role[])=>SetMetadata(ROLES_KEY, roles);
Bien, ahora podremos definir en los Endpoints que roles queremos que puedan acceder a los Endpoints, veamos como hacer esto en los controladores:
// nos traemos al decoradorimport{Roles}from'src/auth/decorators/roles.decorator';// nos traemos al modeloimport{Role}from'src/auth/models/roles.model';// ...@UseGuards(JwtAuthGuard)@ApiTags('products')@Controller('products')exportclassProductsController{// ...// con este decorador indicamos que solo los administradores pueden crear un producto @Post()create(@Body() payload:CreateProductDto){returnthis.productsService.create(payload);}}
Esto solo en parte, porque aún no hemos programado la lógica para que esto funcione, solo estamos enviando la metadata al Endpoint, así que vamos a programar la lógica para que esto funcione:
Para esto vamos a crear un nuevo guardian en el que si el usuario Autenticado tiene el rol, le damos acceso, y si no tiene el rol, no le damos acceso:
Para esto vamos a nuestra terminal y ejecutamos el siguiente comando:
nest g gu auth/guards/roles
Ahora manejemos la lógica de este nuevo guardian:
import{CanActivate,ExecutionContext,Injectable,UnauthorizedException,}from'@nestjs/common';// importamos al reflector, qu es el que nos trae esta metadataimport{Reflector}from'@nestjs/core';import{Observable}from'rxjs';// nos traemos a la key de los rolesimport{ROLES_KEY}from'../decorators/roles.decorator';// nos traemos los modelos de los rolesimport{Role}from'../models/roles.model';// nos traemos al modelo de autenticación de usuarioimport{PayloadToken}from'../models/token.model';@Injectable()exportclassRolesGuardimplementsCanActivate{constructor(private reflector:Reflector){}canActivate( context:ExecutionContext,):boolean|Promise<boolean>|Observable<boolean>{// obtenemos los roles de la metadata, nos los dsrán como un arrar de rolesconst roles:Role[]=this.reflector.get(ROLES_KEY, context.getHandler());// preguntamos si en la metadata recibimos algún rol, si no hay rolesif(!roles){// lo dejamos pasar sin másreturntrue;}// obtenemos el Requestconst request = context.switchToHttp().getRequest();// del request, quiero obtener al usuarioconst user = request.userasPayloadToken;// verificamos su el usuario autentificado tiene permisos para usar el Endpointconst isAuth = roles.some((role)=> role === user.role);// si el usuario no tiene permisos para entrarif(!isAuth){// retornamos un error de no autorizadothrownewUnauthorizedException('Your role is not accepted >:(');}// tiene permisos lo dejamos pasarreturn isAuth;}}
Ahora en nuestro controlador tenemos que utilizar al guardián:
// importamos el nuevo guadianimport{RolesGuard}from'src/auth/guards/roles.guard';// lo usamos en el controlador@UseGuards(...,RolesGuard)@ApiTags('products')@Controller('products')exportclassProductsController{// ...}
Y listo, de esta forma ya estamos manejando roles en nuestra aplicación con Nest.js y tenemos una lógica de negocio firme :3.
Esto es ya que como los endpoint públicos sólo tienen el decorador @Public, por tanto al tratar de obtener roles usando el reflector, retorna undefined
const roles =this.reflector.get<Role[]>(ROLES_KEY, context.getHandler());// roles es undefinedconst request = context.switchToHttp().getRequest();const user = request.userasPayloadToken;const isAuth = roles.some((role)=> role === user.role);// roles.some dara un error del tipo TypeError: Cannot read property 'some' of undefined
Una solución rápida es tambien verificar en el RolesGuard si el endpoint es publico quedando el metodo de esta forma:
import{CanActivate,ExecutionContext,Injectable,UnauthorizedException,}from'@nestjs/common';import{Reflector}from'@nestjs/core';import{Observable}from'rxjs';import{IS_PUBLIC_KEY}from'../decorators/public.decorator';import{ROLES_KEY}from'../decorators/roles.decorator';import{Role}from'../models/roles.models';import{PayloadToken}from'../models/token.model';@Injectable()exportclassRolesGuardimplementsCanActivate{constructor(privatereflector:Reflector){}canActivate(context:ExecutionContext,): boolean |Promise<boolean>|Observable<boolean>{// Verify if the endpoint has the Public decoratorconst isPublic =this.reflector.get(IS_PUBLIC_KEY, context.getHandler());if(isPublic)returntrue;// Get the roles from the endpointconst roles =this.reflector.get<Role[]>(ROLES_KEY, context.getHandler());const request = context.switchToHttp().getRequest();const user = request.userasPayloadToken;const isAuth = roles.some((role)=> role === user.role);if(!isAuth){thrownewUnauthorizedException('Can not access to this resource, wrong rol',);}returntrue;}}