Saltar al contenido principal

RBAC en la práctica: Implementando autorización segura para tu aplicación

¿Tienes dificultades para implementar un sistema de autorización seguro y escalable para tu aplicación? El control de acceso basado en roles (RBAC) es el estándar de la industria para gestionar los permisos de los usuarios, pero implementarlo correctamente puede ser un reto. Este tutorial te mostrará cómo construir un sistema RBAC robusto usando un ejemplo real de un sistema de gestión de contenidos (CMS).

Siguiendo esta guía, aprenderás:

  • ✨ Cómo diseñar e implementar permisos granulares que te den control preciso
  • 🔒 Mejores prácticas para organizar permisos en roles significativos
  • 👤 Técnicas para gestionar la propiedad de recursos de manera efectiva
  • 🚀 Formas de hacer que tu sistema de autorización sea escalable y mantenible
  • 💡 Implementación práctica usando un ejemplo real de CMS

El código fuente completo de este tutorial está disponible en GitHub.

Entendiendo los fundamentos de RBAC

El control de acceso basado en roles es más que solo asignar permisos a los usuarios. Se trata de crear un enfoque estructurado para la autorización que equilibre la seguridad con la mantenibilidad.

Puedes aprender más sobre ¿Qué es RBAC? en Auth Wiki.

Estos son los principios clave que seguiremos en nuestra implementación:

Diseño de permisos granulares

Los permisos granulares te dan control preciso sobre lo que los usuarios pueden hacer en tu sistema. En lugar de niveles de acceso amplios como "admin" o "usuario", definimos acciones específicas que los usuarios pueden realizar sobre los recursos. Por ejemplo:

  • read:articles - Ver cualquier artículo en el sistema
  • create:articles - Crear nuevos artículos
  • update:articles - Modificar artículos existentes
  • publish:articles - Cambiar el estado de publicación de los artículos

Propiedad de recursos y control de acceso

La propiedad de recursos es un concepto fundamental en el diseño de autorización de nuestro CMS. Mientras que RBAC define qué acciones pueden realizar los diferentes roles, la propiedad añade una dimensión personal al control de acceso:

  • Los autores tienen acceso automáticamente a los artículos que crearon
  • Este modelo de propiedad natural significa que los autores siempre pueden ver y editar su propio contenido
  • El sistema verifica tanto los permisos de rol O la propiedad al manejar operaciones sobre artículos
  • Por ejemplo, incluso sin el permiso update:articles, un autor puede editar sus propios artículos
  • Este diseño reduce la necesidad de permisos extra de rol mientras mantiene la seguridad

Este enfoque de doble capa (roles + propiedad) crea un sistema más intuitivo y seguro. Los editores y administradores pueden seguir gestionando todo el contenido a través de sus permisos de rol, mientras que los autores mantienen el control sobre su propio trabajo.

Diseñando APIs seguras

Comencemos diseñando la funcionalidad principal de nuestro CMS a través de sus endpoints de API:

GET    /api/articles         # Listar todos los artículos
GET /api/articles/:id # Obtener un artículo específico
POST /api/articles # Crear un nuevo artículo
PATCH /api/articles/:id # Actualizar un artículo
DELETE /api/articles/:id # Eliminar un artículo
PATCH /api/articles/:id/published # Cambiar el estado de publicación

Implementar control de acceso para tu API

Para cada endpoint, necesitamos considerar dos aspectos del control de acceso:

  1. Propiedad del recurso - ¿El usuario es dueño de este recurso?
  2. Permisos basados en roles - ¿El rol del usuario permite esta operación?

Así es como gestionaremos el acceso para cada endpoint:

EndpointLógica de control de acceso
GET /api/articles- Cualquiera con el permiso list:articles, O autores pueden ver sus propios artículos
GET /api/articles/:id- Cualquiera con el permiso read:articles, O autor del artículo
POST /api/articles- Cualquiera con el permiso create:articles
PATCH /api/articles/:id- Cualquiera con el permiso update:articles, O autor del artículo
DELETE /api/articles/:id- Cualquiera con el permiso delete:articles, O autor del artículo
PATCH /api/articles/:id/published- Solo usuarios con el permiso publish:articles

Crear un sistema de permisos que escale

Según los requisitos de acceso de nuestra API, podemos definir estos permisos:

PermisoDescripción
list:articlesVer la lista de todos los artículos en el sistema
read:articlesLeer el contenido completo de cualquier artículo
create:articlesCrear nuevos artículos
update:articlesModificar cualquier artículo
delete:articlesEliminar cualquier artículo
publish:articlesCambiar el estado de publicación

Ten en cuenta que estos permisos solo son necesarios al acceder a recursos que no posees. Los propietarios de los artículos pueden automáticamente:

  • Ver sus propios artículos (no se necesita read:articles)
  • Editar sus propios artículos (no se necesita update:articles)
  • Eliminar sus propios artículos (no se necesita delete:articles)

Construyendo roles efectivos

Ahora que tenemos nuestra API y permisos definidos, podemos crear roles que agrupen estos permisos lógicamente:

Permiso/Rol👑 Admin📝 Publisher✍️ Author
DescripciónAcceso total al sistema para la gestión completaPuede ver todos los artículos y controlar la publicaciónPuede crear nuevos artículos en el sistema
list:articles
read:articles
create:articles
update:articles
delete:articles
publish:articles

Nota: Los autores tienen automáticamente permisos de lectura / actualización / eliminación para sus propios artículos, independientemente de los permisos de rol.

Cada rol está diseñado con responsabilidades específicas en mente:

  • Admin: Tiene control total sobre el CMS, incluidas todas las operaciones sobre artículos
  • Publisher: Se enfoca en la revisión de contenido y la gestión de la publicación
  • Author: Se especializa en la creación de contenido

Esta estructura de roles crea una clara separación de responsabilidades:

  • Los autores se enfocan en crear contenido
  • Los editores gestionan la calidad y visibilidad del contenido
  • Los administradores mantienen el control general del sistema

Configurar RBAC en Logto

Antes de comenzar, necesitas crear una cuenta en Logto Cloud, o también puedes usar una instancia autogestionada de Logto utilizando la versión Logto OSS.

Pero para este tutorial, usaremos Logto Cloud por simplicidad.

Configurando tu aplicación

  1. Ve a "Applications" en Logto Console para crear una nueva aplicación react

Aplicación React CMS

Configurando recursos de API y permisos

  1. Ve a "API Resources" en Logto Console para crear un nuevo recurso de API
    • Nombre de la API: CMS API
    • Identificador de la API: https://api.cms.com
    • Añade permisos al recurso de API
      • list:articles
      • read:articles
      • create:articles
      • update:articles
      • publish:articles
      • delete:articles

Detalles del recurso de API CMS

Creando roles

Ve a Roles en Logto Console para crear los siguientes roles para el CMS

  • Admin
    • con todos los permisos
  • Publisher
    • con read:articles, list:articles, publish:articles
  • Author
    • con create:articles

Rol Admin

Rol Publisher

Rol Author

Asignando roles a los usuarios

Ve a la sección "User management" en Logto Console para crear usuarios.

En la pestaña "Roles" de los detalles del usuario, puedes asignar roles al usuario.

En nuestro ejemplo, creamos 3 usuarios con los siguientes roles:

  • Alex: Admin
  • Bob: Publisher
  • Charlie: Author

Gestión de usuarios

Detalles de usuario - Alex

nota:

Para fines de demostración, creamos estos recursos y configuraciones a través de Logto Console. En proyectos reales, puedes crear estos recursos y configuraciones programáticamente usando la Management API proporcionada por Logto.

Integra tu frontend con Logto RBAC

Ahora que hemos configurado RBAC en Logto, podemos comenzar a integrarlo en nuestro frontend.

Primero, sigue los Logto Quick Starts para integrar Logto en tu aplicación.

En nuestro ejemplo, usamos React para la demostración.

Después de configurar Logto en tu aplicación, necesitamos añadir las configuraciones de RBAC para que Logto funcione.

// frontend/src/App.tsx

const logtoConfig: LogtoConfig = {
appId: LOGTO_APP_ID, // El ID de la app que creaste en Logto Console
endpoint: LOGTO_ENDPOINT, // El endpoint que creaste en Logto Console
resources: [API_RESOURCE], // El identificador del recurso de API que creaste en Logto Console, por ejemplo https://api.cms.com
// Todos los alcances que puedas querer solicitar del recurso de API en el frontend
scopes: [
'list:articles',
'create:articles',
'read:articles',
'update:articles',
'delete:articles',
'publish:articles',
],
};

Recuerda cerrar sesión y volver a iniciar sesión para que este cambio tenga efecto si ya has iniciado sesión.

Cuando el usuario inicia sesión con Logto y solicita un token de acceso para los recursos de API especificados arriba, Logto añadirá los alcances (permisos) relacionados con el rol del usuario al token de acceso.

Puedes usar getAccessTokenClaims del hook useLogto para obtener los alcances del token de acceso.

// frontend/src/hooks/use-user-data.ts

import { useLogto } from '@logto/react';
import { API_RESOURCE } from '../config';
import { useState, useEffect } from 'react';

export const useUserData = () => {
const { getAccessTokenClaims } = useLogto();
const [userScopes, setUserScopes] = useState<string[]>([]);
const [userId, setUserId] = useState<string>();

useEffect(() => {
const fetchScopes = async () => {
const token = await getAccessTokenClaims(API_RESOURCE);
setUserScopes(token?.scope?.split(' ') ?? []);
setUserId(token?.sub);
};

fetchScopes();
}, [getAccessTokenClaims]);

return { userId, userScopes };
};

Y puedes usar userScopes para comprobar si el usuario tiene permiso para acceder al recurso.

// frontend/src/pages/Dashboard.tsx

const Dashboard = () => {
const { userId, userScopes } = useUserData();
// ...

return (
<div>
{/* ... */}
{(userScopes.includes('delete:articles') || article.ownerId === userId) && (
<button
onClick={() => handleDelete(article.id)}
className="text-red-600 hover:text-red-900"
>
Delete
</button>
)}
</div>
);
};

Integra tu backend con Logto RBAC

Ahora, es momento de integrar Logto RBAC en tu backend.

Middleware de autorización en el backend

Primero, necesitamos añadir un middleware en el backend para comprobar los permisos del usuario, verificar si el usuario ha iniciado sesión y determinar si tiene los permisos necesarios para acceder a ciertas APIs.

// backend/src/middleware/auth.js

const { createRemoteJWKSet, jwtVerify } = require('jose');

const getTokenFromHeader = (headers) => {
const { authorization } = headers;
const bearerTokenIdentifier = 'Bearer';

if (!authorization) {
throw new Error('Authorization header missing');
}

if (!authorization.startsWith(bearerTokenIdentifier)) {
throw new Error('Authorization token type not supported');
}

return authorization.slice(bearerTokenIdentifier.length + 1);
};

const hasScopes = (tokenScopes, requiredScopes) => {
if (!requiredScopes || requiredScopes.length === 0) {
return true;
}
const scopeSet = new Set(tokenScopes);
return requiredScopes.every((scope) => scopeSet.has(scope));
};

const verifyJwt = async (token) => {
const JWKS = createRemoteJWKSet(new URL(process.env.LOGTO_JWKS_URL));

const { payload } = await jwtVerify(token, JWKS, {
issuer: process.env.LOGTO_ISSUER,
audience: process.env.LOGTO_API_RESOURCE,
});

return payload;
};

const requireAuth = (requiredScopes = []) => {
return async (req, res, next) => {
try {
// Extraer el token
const token = getTokenFromHeader(req.headers);

// Verificar el token
const payload = await verifyJwt(token);

// Añadir información del usuario a la petición
req.user = {
id: payload.sub,
scopes: payload.scope?.split(' ') || [],
};

// Verificar los alcances requeridos
if (!hasScopes(req.user.scopes, requiredScopes)) {
throw new Error('Insufficient permissions');
}

next();
} catch (error) {
res.status(401).json({ error: 'Unauthorized' });
}
};
};

module.exports = {
requireAuth,
hasScopes,
};

Como puedes ver, en este middleware verificamos si la petición del frontend contiene un token de acceso válido y comprobamos si la audiencia del token de acceso coincide con el recurso de API que creamos en Logto Console.

La razón para verificar el recurso de API es que nuestro recurso de API realmente representa los recursos del backend de nuestro CMS, y todos nuestros permisos del CMS están asociados a este recurso de API.

Dado que este recurso de API representa los recursos del CMS en Logto, en nuestro código frontend, incluimos el correspondiente token de acceso al hacer peticiones API al backend:

// frontend/src/hooks/use-api.ts
export const useApi = () => {
const { getAccessToken } = useLogto();

return useMemo(
() =>
async (endpoint: string, options: RequestInit = {}) => {
try {
// Obtener el token de acceso para el recurso de API
const token = await getAccessToken(API_RESOURCE);

if (!token) {
throw new ApiRequestError('Failed to get access token');
}

const response = await fetch(`${API_BASE_URL}${endpoint}`, {
...options,
headers: {
'Content-Type': 'application/json',
// Añadir el token de acceso a las cabeceras de la petición
Authorization: `Bearer ${token}`,
...options.headers,
},
});

// ... manejar la respuesta

return await response.json();
} catch (error) {
// ... manejo de errores
}
},
[getAccessToken]
);
};

Ahora podemos usar el middleware requireAuth para proteger nuestros endpoints de API.

Protegiendo endpoints de API

Para las APIs que solo deben ser accesibles por usuarios con permisos específicos, podemos añadir restricciones directamente en el middleware. Por ejemplo, la API de creación de artículos solo debe ser accesible para usuarios con el permiso create:articles:

// backend/src/routes/articles.js

const { requireAuth } = require('../middleware/auth');

router.post('/articles', requireAuth(['create:articles']), async (req, res) => {
// ...
});

Para las APIs que necesitan comprobar tanto permisos como propiedad del recurso, podemos usar la función hasScopes. Por ejemplo, en la API de listado de artículos, los usuarios con el alcance list:articles pueden acceder a todos los artículos, mientras que los autores pueden acceder a los artículos que ellos mismos crearon:

// backend/src/routes/articles.js

const { requireAuth, hasScopes } = require('../middleware/auth');

router.get('/articles', requireAuth(), async (req, res) => {
try {
// Si el usuario tiene el alcance list:articles, devolver todos los artículos
if (hasScopes(req.user.scopes, ['list:articles'])) {
const articles = await articleDB.list();
return res.json(articles);
}

// De lo contrario, devolver solo los artículos del usuario
const articles = await articleDB.listByOwner(req.user.id);
res.json(articles);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch articles' });
}
});

En este punto, hemos completado la implementación de RBAC. Puedes consultar el código fuente completo para ver la implementación completa.

Prueba la implementación de RBAC en el CMS

Ahora, probemos nuestra implementación de RBAC en el CMS usando los tres usuarios que acabamos de crear.

nota:

Si ves que no puedes iniciar sesión con las credenciales de los usuarios creados en "User Management", primero tendrás que habilitar el método de inicio de sesión adecuado. Ve a "Sign-in Experience" en Logto Console y habilita tu método de autenticación preferido (como Email + Password o Username + Password).

Primero, iniciemos sesión como Alex y Charles respectivamente y creemos algunos artículos.

Como Alex tiene el rol de Admin, puede crear, eliminar, actualizar, publicar y ver todos los artículos.

Panel CMS - Alex

Charles, con el rol de Author, solo puede crear sus propios artículos y solo puede ver, actualizar y eliminar los artículos que posee.

Panel CMS - Charles - Lista de artículos

Bob, con el rol de Publisher, puede ver y publicar todos los artículos pero no puede crearlos, actualizarlos ni eliminarlos.

Panel CMS - Bob

Conclusión

¡Felicidades! Has aprendido cómo implementar un sistema RBAC robusto en tu aplicación.

Para escenarios más complejos, como construir aplicaciones multi-tenant, Logto proporciona soporte completo para organizaciones. Consulta nuestra guía Construye una aplicación SaaS multi-tenant: Una guía completa desde el diseño hasta la implementación para aprender más sobre cómo implementar control de acceso a nivel organizacional.

¡Feliz programación! 🚀