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.
import{CanActivate,ExecutionContext,ForbiddenException,Injectable,}from'@nestjs/common';import{Observable}from'rxjs';import{Reflector}from'@nestjs/core';import{ROLES_KEY}from'../decorators/roles.decorator';import{PayloadToken}from'../models/token.model';import{Role}from'../models/roles.model';@Injectable()exportclassRolesGuardimplementsCanActivate{constructor(privatereflector:Reflector){}canActivate(context:ExecutionContext,): boolean |Promise<boolean>|Observable<boolean>{const roles =this.reflector.get<Role[]>(ROLES_KEY, context.getHandler());if(!roles){returntrue;}const request = context.switchToHttp().getRequest();const user = request.userasPayloadToken;const isAuth = roles.includes(user.roleasRole);if(!isAuth){thrownewForbiddenException('your role is wrong');}return isAuth;}}
Justo cuando uso some, pensé en includes, es mas sencillo y sigue cumpliendo con la lógica que necesitamos!
Por buena practica, cuando el rol no es el correcto se debe devolver es un 403 - Forbidden, en vez de un 401 - Unauthorized
porque 403?
Efectivamente, dejo la explicación desde la MDN:
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."
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.
ty!
Pueden usar el decorador HttpExeption() y con HttpStatus escoger el tipo de estatus quieren devolver, dependiendo de la respuesta. Por ejemplo:
if(!isAuth){thrownewHttpException('Forbidden, Your role is Wrong',HttpStatus.FORBIDDEN,);}return isAuth;
Tambien podrías importar ForbiddenException de @nestjs/common y enviar tu mensaje personalizado
Hola, en el caso del logout como se realizaria?
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.
hola, me podrian indicar como seria la estrategia para roles dinamicos?
nest g gu auth/guards/roles --no-spec
El --no-spec con que objetivo es? entiendo que no genera el archivo .spec pero cual es la diferencia entre tenerlo y no tenerlo?
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
Como seria la implentacion de un test unitario para los guard?
Hola, tengo otra duda/problema. El reflector.get me retorna un booleano, y no el arreglo.
Este es mi código:
Incluso traté de tipar con requiredRoles: UserRole[], pero aún así, me retorna un boolean, y el consiguiente error:
[Nest]66320-10/17/2022,11:57:32AMERROR[ExceptionsHandler] requiredRoles.some is not a function
¿Alguna idea de cual puede ser el error?
En mi caso tengo 3 tipos de usuarios pero los he dividido en 3 documentos diferentes,
users(para usuarios administradores)
clients(para clientes que compran en el negocio)
owners(para los dueños de los neogicos)
¿Cómo lo podria manejarlo de la forma MÁS CONVENIENTE, ya que todos NO pertenecen al mismo documento?
hola, como seria la estrategia para Roles dinamicos ?
ahora al utilizar los endpoints publicos me da el siguiente error
{"statusCode":500,"message":"Internal server error"}
si quito RolesGuard si funcionan mis endpoints publicos
@UseGuards(JwtAuthGuard,RolesGuard)
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;}}
nest g gu auth/guards/roles --flat --no-spec
Igual podríamos validar isPublic y cómo ultimo recurso el array vacío.