¿Cómo gestionar los permisos por roles en una API segura?
La correcta gestión de permisos es crucial al desarrollar sistemas seguros. No basta con autenticar usuarios; también es imprescindible controlar quién puede realizar ciertas acciones dentro de la aplicación. En este artículo, exploraremos cómo manejar roles de usuario y proteger endpoints en una API usando autenticación JWT (JSON Web Token).
¿Por qué es esencial la gestión de roles?
En un sistema típico, no todos los usuarios deben tener las mismas capacidades. Por ejemplo, solo los administradores deberían poder crear o modificar productos, mientras que los clientes podrían limitarse a acciones como registrarse y crear órdenes de compra. La gestión de roles permite:
Restringir accesos: Solo usuarios autorizados pueden realizar ciertas acciones.
Mantener seguridad y orden: Minimiza riesgos de acciones no deseadas por parte de usuarios.
¿Cómo implementar middleware para roles en JWT?
Agregaremos un middleware para verificar si el usuario que realiza una petición tiene el rol adecuado.
Para hacer esta verificación, aprovechamos que después de la autenticación JWT, el payload desencriptado está disponible a través de req.user.
¿Cómo manejar múltiples roles de forma dinámica?
Crear un middleware específico por cada rol puede volverse complicado. En su lugar, podemos usar un enfoque más flexible con una función que permita el acceso a varios roles:
Con esta función, podemos especificar qué roles tienen permiso para acceder a un determinado endpoint, simplificando la gestión de permisos.
¿Cómo utilizar y probar estos middleware?
Definir roles en tus rutas: Al definir las rutas en tu aplicación, especifica qué roles tienen acceso.
app.get('/category',checkRoles('admin','seller'),(req, res)=>{// Código para devolver categorías});
Probar con herramientas como Insomnia: Al enviar solicitudes utilizando tokens de usuario con diferentes roles (admin, cliente, etc.), verifica que cada usuario solo accede a lo que debiera.
¿Existen soluciones más avanzadas?
Sí, para aplicaciones donde los requisitos de permisos son más complejos, puedes considerar utilizar librerías como Access Control, que ofrece una forma más robusta de gestionar permisos de usuario basados en roles y acciones.
¿Qué otras consideraciones deberías tener?
Endpoints públicos: Decide cuidadosamente cuáles deben estar protegidos y cuáles pueden ser accesibles sin autenticación.
Gestión de errores: Proporciona mensajes claros cuando un usuario no está autorizado para acceder a un recurso.
La implementación de un sistema de roles bien estructurado es una base sólida para la seguridad de tu API, permitiendo un control granular sobre lo que cada clase de usuario puede y no puede hacer. Continúa explorando estas best practices para asegurar que tu aplicación se mantiene segura y eficiente.
Yo diria que es un 403, no tiene permisos de acceso. 401 yo lo uso solo para si es sesion o no.
pienso lo mismo, el mio lo deje asi:
boom.forbidden('se requieren permisos de administrador')
Es correcto tu razonamiento:
.
"401 Unauthorized is the status code to return when the client provides no credentials or invalid credentials. 403 Forbidden is the status code to return when a client has valid credentials but not enough privileges to perform an action on a resource."
.
Fuente
Se debe trabajar en la gestión de permisos y roles ya que no todos deben poder crear categorías o crear usuarios, únicamente un usuario administrador podría hacer eso.
Se crea un middleware que verifique que tipo de rol es, y lo que lo deje seguir o no.
En auth.handler.js se crea la función checkAdminRole la cual verificará si el rol del usuario es admin o customer, si es admin entonces pasa al siguiente middleware, de lo contrario lanza un error unauthorized.
functioncheckAdminRole(req, res, next){const user = req.user;if(user.role==='admin'){next();}else{next(boom.unauthorized());}}
A la ruta se le agrega el middleware, la lógica es:
Autenticar, verificar el token y obtener los datos del user (*passport.authenticate)*.
Verificar el tipo de rol de user (checkAdminRole).
Cuando se desea escalar y tener más roles, la función checkAdminRole se vuelve poco mantenible, por ello creamos la función checkRoles que recibirá los roles que tendrán acceso a ese endpoint. Si en los roles se encuentra el rol del usuario, devolverá true y tendrá acceso al endpoint, de lo contrario devolverá false y arrojará error unauthorized.
En resumen, la función checkRoles recibe un array de roles, verifica que user.role se encuentre en ese array, y si todo bien procede al siguiente middleware.
functioncheckRoles(...roles){return(req, res, next)=>{const user = req.user;if(roles.includes(user.role)){next();}else{next(boom.unauthorized());}};}
Haciendo la implementación del middleware en la ruta queda de la siguiente manera, agregando los roles a los que tendrá acceso ese endpoint.
Se recomienda utilizar la librería accesscontrol donde realmente y de forma explicita se gestionan permisos de una forma más profunda y avanzada.
To create a role control, a middleware will be our best friend
First of all, let's think about what we need, we need a middleware able to check what kind of users is authenticated, for this, let's create a single function, which will receive a list of roles and return a middleware.
//@param roles array of roles that can access the routefunctioncheckRole(roles){return(req, res, next)=>{const user = req.user;//Check if rol is allowed to accessif(!roles.includes(user.role)){next(boom.unauthorized("Unauthorized!You cannot do this, Admins have been notified "));}else{//If everything is right, go to next middlewarenext();}}}
Finally just use it wherever you need to check role, por example:
router.post("/", passport.authenticate('jwt',{session:false}),checkRole(['admin','Captain']),validatorHandler(getOrderSchema,"body"),async(req, res, next)=>{try{const body = req.body; res.status(201).json(await service.create(body));}catch(error){next(error);}});
Recommendation
Acces Control NPM
Haciendo que esto sea un poco más escalable y mejores prácticas:
Creé un archivo index.js dentro de una carpeta 'roles' y simplemente es esto:
Si estan usando insomia para hacer la pruebas de la API, pueden guardar el token del usuario al hacer login. Pega la siguiente linea, dentro del archivo de variables de entorno
Nota: después de pegar la línea de código aparecerá un recuadro en rojo, no se asusten, deben configurar los parámetros según sus request disponible (esto se hace a traves de una ventana gráfica)
Function to Perform: deber ser response (origen de la info que se desea guardar)
Attribute: body ( que parte especifica del response contiene la info)
Request: [Auth] POST Login Admin, este el nombre de mi request
Filter: este es valor que deseamos guardar en la variable de entorno, en este caso es $.token
Trigger behavior: acción a realizar una vez obtenida la info, en este caso No History, ya que solo queremos las info guardada en token
Este tip está genial!! Muchas gracias
Muchas gracias.
No se si alguien ya lo puso antes, pero hice un pequeño cambio al código para no poner un rol que de por si tendrá acceso a todo ( al menos en este caso) para no repetir "admin" cada que se llama a la función hice lo siguiente:
functioncheckRoles(...roles){ roles.push('admin');return(req, res, next)=>{const user = req.user;if(roles.includes(user.role)){next()}else{next(boom.unauthorized());}}}
Lo probé así, borre los argumentos 'admin' y permitió que un Customer pudiese crear una categoria. Así que por favor cuidado con ese push.
Es correcto lo que menciona Ricardo, no deberías hacer ese push en el middleware, ya que le estás dando permisos de admin a todas las peticiones.
👮♀ Clase #12: Control de roles 12/20 👮
Pasos: 📝
Vamos a implementar un control para el acceso de las peticiones, es decir, si se establece que según el tipo de role (‘admin’, ‘customer’, ‘seller’) tiene privilegios para acceder a las rutas como crear categorías, ver las categorías almacenadas, editar y eliminar categorías.
Para ello, vamos a ++VSC++, entramos a la carpeta middlewares, abrimos el archivo auth.handler.js, se implementan las funciones para chequear si el role del usuario que hizo el login tiene permitido hacer la petición, las funciones checkAdminRole y checkRoles quedan:
//Ésta función solo evalúa para el role ‘admin’functioncheckAdminRole(req,res,next){console.log(req.user);//Se encuentra el payloadconst user=req.user;if(user.role==='admin'){next();}else{next(boom.unauthorized());}}//Los 3 puntos transorma todo argumento en arrayfunctioncheckRoles(...roles){return(req,res,next)=>{const user=req.user;//Compara el role del usuario con los roles permitidos enviadosif(roles.includes(user.role)){next();}else{next(boom.unauthorized());}}}module.exports={checkApiKey,checkAdminRole,checkRoles}
Guardamos, vamos a la carpeta routes y abrimos el archivo categories.router.js, se implementa la lógica en cada petición, el código queda:
const express =require('express');const passport =require('passport');constCategoryService=require('./../services/category.service');const validatorHandler =require('./../middlewares/validator.handler');//Para veriicar si tiene autorización:const{ checkRoles }=require('./../middlewares/auth.handler');//const { checkAdminRole } = require('./../middlewares/auth.handler');const{ createCategorySchema, updateCategorySchema, getCategorySchema }=require('./../schemas/category.schema');const router = express.Router();const service =newCategoryService();router.get('/', passport.authenticate('jwt',{session:false}),checkRoles('admin','seller','customer'),async(req, res, next)=>{try{const categories =await service.find(); res.json(categories);}catch(error){next(error);}});router.get('/:id', passport.authenticate('jwt',{session:false}),checkRoles('admin','seller','customer'),validatorHandler(getCategorySchema,'params'),async(req, res, next)=>{try{const{ id }= req.params;const category =await service.findOne(id); res.json(category);}catch(error){next(error);}});router.post('/',//Proteger este endpoint con passport passport.authenticate('jwt',{session:false}),//Verificar si está autorizado con el role de 'admin'//checkAdminRole,checkRoles('admin'),validatorHandler(createCategorySchema,'body'),async(req, res, next)=>{try{const body = req.body;const newCategory =await service.create(body); res.status(201).json(newCategory);}catch(error){next(error);}});router.patch('/:id', passport.authenticate('jwt',{session:false}),checkRoles('admin','seller'),validatorHandler(getCategorySchema,'params'),validatorHandler(updateCategorySchema,'body'),async(req, res, next)=>{try{const{ id }= req.params;const body = req.body;const category =await service.update(id, body); res.json(category);}catch(error){next(error);}});router.delete('/:id', passport.authenticate('jwt',{session:false}),checkRoles('admin','seller'),validatorHandler(getCategorySchema,'params'),async(req, res, next)=>{try{const{ id }= req.params;await service.delete(id); res.status(201).json({id});}catch(error){next(error);}});module.exports= router;
Guardamos, si no se ha compilado con: npm run dev en la terminal, esperar a Mi port 3000
Vamos a ++Insomnia++, entramos a la carpeta Auth, duplicamos el Login y lo editamos y colocamos en uno: Login Admin y Login Customer.
Consultamos en la petición ++POST++ de la carpeta Customer el último cliente creado, y con los datos hacemos un nuevo Login en Auth, el body en mi caso:
Con ese token lo guardamos en Manage Environment, vamos ala carpeta Categories, luego a Get Categories, en la pestaña Auth, se selecciona Bearer y en TOKEN se coloca el token generado recientemente, al dar a Send, debe aparecer el código 200 OK con la lista de las Categorías almacenadas, en caso de que el cliente no tenga el role con privilegio, sale el código 401 Unauthorized.
AccessControl
no estoy seguro si de esta manera será la mejor practica, pero a mí me funcionó XD
Como capturo el unauthorized!, en vez de solo texto sino como un json con mas informacion
Algo que me acabe de dar cuenta es que nostros podemos ejecutar cuantos middleware queramos , pasandolos por comas, pero esto hara que nuestro codigo sea mas desordenado. Por lo cual mi recomendacion es meterlos dentro de un arreglo para mantener el orden.
Hola!
Si no usamos los tres puntos funcionaria?
Como ?
Esto es muy bueno para dar control de roles desde el back-end, pero si estoy tratando de integrar un front-end a mi API: ¿cómo le paso un bearer token?
¿algún hook en react o cómo?
Una manera puede ser enviar el token como una cookie, de este modo no tienes casi nada de que preocuparte en el front-end ya que puedes configurar la cookie para que se envíe de regreso al back-end en cada petición y allí accedes a esa cookie para validar información del usuario, tal como los roles.
Si también quieres restringir ciertas rutas en el front-end una opción podría ser además de la cookie enviar algunos datos del usuario dentro de un json como respuesta al login, podrías enviar los roles y eso lo manejas con un contexto o hook en react.
Suena muy bien tu aporte, ¿tienes algún código de referencia donde pueda indagar? No he trabajado con cookies en js
donde esta el repo de este curso 👀?
Hola juan ! este es el link del repo:
recuerda que es la estructura del proyecto proviene del curso de PostgreSQL con Node
😀Con toda
creo que la manera de obtener el body del objeto usuario o customer es inseguro, porque cualquiera puede llegar a modificar el rol del usuario, ¿no sería mejor declarar campo por campo y quitar el rol para que no pueda ser modificado? ó ¿hay otra manera de evitar que el cualquiera cambia el rol al momento de crear un usuario?
buena clase, pero me parece que la mayoria de estas validaciones deberían hacerse a nivel de base datos, casi todo lo quieren hacer en el servidor pero se olviden que existen otros componentes que pueden delegar estas actividades y cuidar el perfomance a nivel general.
Ni idea del por qué postman se cada cargando eternamente a pesar de que el servidor resuelva que es un 403 u.u