Identidad Distribuida: La Arquitectura de Confianza en Sistemas Multi-Tenant

Identidad Distribuida: La Arquitectura de Confianza en Sistemas Multi-Tenant

Cómo diseñar autenticación, autorización y tokens en sistemas multi-tenant distribuidos. JWT, firma asimétrica y aislamiento de datos.

Por Omar Flores

Imagina que trabajas en un sistema monolítico. La sesión de un usuario vive en un simple map[sessionID]UserData en memoria. Un solo proceso, una sola máquina, una sola verdad. Funciona perfecto.

Ahora imagina que ese sistema se fragmenta en quince servicios independientes. Cada uno con su propia base de datos, cada uno desplegado en su propio contenedor, cada uno hablando con los demás por red. La memoria compartida desaparece. El mapa ya no existe. Y cada vez que un usuario hace un request, cada servicio necesita responder una pregunta que antes era trivial: ¿quién es esta persona y qué puede hacer aquí?

Este es el problema de la confianza distribuida. Y tiene exactamente dos soluciones fundamentales.

ParadigmaMecanismoAnalogía física
Referencia (stateful)Un ID opaco que apunta a estado remotoComo un número de serie que debes validar en fábrica
Auto-contenido (stateless)Un documento firmado criptográficamenteComo un pasaporte: lo verificas tú mismo con la firma

Toda la arquitectura moderna mezcla estos dos paradigmas estratégicamente. Entender cuándo usar cuál es la competencia clave. Y en este artículo vamos a desmenuzar exactamente cómo hacerlo.


Los tres planos: separación de responsabilidades absolutas

El error más común es mezclar tres conceptos que deben vivir en capas completamente separadas. La confusión entre ellos es la raíz de casi todo el dolor de diseño en sistemas de identidad.

Identidad: quién eres en el universo del sistema

La identidad es una afirmación sobre la existencia de una entidad. No dice nada sobre qué puede hacer. Una entidad puede ser un humano (usuario), una empresa (tenant), o un proceso (servicio). La identidad se establece una vez durante el registro y cambia raramente. Es el sustantivo del sistema.

La identidad debe ser estable e inmutable en su forma canónica. Tu RFC es un identificador de identidad perfecto porque es externo, oficial y no cambia. El error sería usar algo mutable como un email como identificador primario, porque entonces los cambios de email rompen toda la cadena de relaciones.

La identidad de un tenant y la identidad de un usuario dentro de ese tenant son jerarquías separadas. Un usuario no existe globalmente en tu sistema —existe dentro del contexto de un tenant. El usuario jperez del RFC ABC123 y el usuario jperez del RFC XYZ789 son entidades completamente distintas que no comparten ningún espacio de identidad.

Autenticación: puedes demostrar que eres quien dices ser

La autenticación es la verificación de una afirmación de identidad. Es un evento puntual en el tiempo, no un estado permanente. Ocurre exactamente una vez por sesión y produce un artefacto de confianza: el token.

La autenticación debe ser centralizada en un único componente —el Authorization Server. Esta centralización no es una conveniencia, es una propiedad de seguridad crítica. Si múltiples componentes pueden autenticar, múltiples componentes pueden ser comprometidos para producir tokens falsos. La superficie de ataque crece linealmente con cada nuevo emisor.

Un flujo de autenticación en un sistema multi-tenant empresarial se ve así: verificar RFC en un catálogo central, luego usuario y contraseña en la base de datos del tenant. La estructura es correcta. El problema es que este flujo suele vivir distribuido o acoplado a un servicio de negocio, cuando debería estar aislado en un componente cuya única responsabilidad es este flujo.

La autenticación también debe ser auditable de forma exhaustiva. Cada intento —exitoso o fallido—, desde qué IP, en qué momento, para qué tenant, debe ser registrado. No es opcional en un sistema multi-tenant empresarial. Es la evidencia forense que necesitas cuando un cliente reporta actividad sospechosa.

Autorización: tienes permiso para hacer esta acción específica

La autorización se evalúa en cada request, no solo en el login. Es donde se aplican los módulos contratados, los roles, los permisos granulares y los límites de uso.

La distinción más importante que la mayoría de los sistemas ignora es esta:

AuthZ de grano grueso vs. grano fino. El grano grueso —¿tiene el módulo de facturación?— puede viajar en el token porque cambia pocas veces. El grano fino —¿puede editar esta factura específica de este cliente?— es estado relacional que cambia constantemente y nunca debe ir en el token.


El Authorization Server como componente autónomo

El Authorization Server es el corazón del sistema. Debe diseñarse como si fuera el componente más crítico de toda la infraestructura, porque lo es.

Su única responsabilidad

Emitir artefactos de confianza verificables después de validar identidad y estado del tenant. No hace negocio. No consulta datos de facturas. No aplica permisos finos. Solo acuña confianza.

Los flujos internos

Cuando llega una solicitud de autenticación, el Authorization Server ejecuta una secuencia de validaciones ordenadas. El orden importa porque cada paso puede terminar el flujo con un error, y el orden determina la información que revelas en los errores —menos información es más seguro.

Primer paso: Validación del tenant. Antes de verificar cualquier credencial de usuario, verifica que el tenant —el RFC— existe, está activo y tiene una suscripción válida. Si el tenant no existe o está suspendido, el flujo termina aquí. Este paso evita que ataques de fuerza bruta sobre credenciales de usuario ocurran contra tenants inactivos. También es donde verificas que la suscripción esté vigente —si un cliente no pagó, su flujo de autenticación falla aquí, antes de llegar al usuario.

Segundo paso: Resolución del almacén de credenciales. Una vez validado el tenant, necesitas saber dónde están sus credenciales. En una arquitectura de base de datos por tenant, esto significa resolver la cadena de conexión a la BD de ese RFC. Esta resolución debe hacerse desde un catálogo confiable, nunca desde datos que el cliente proporcione.

Tercer paso: Verificación de credenciales del usuario. Con la conexión al almacén correcto, verificas que el usuario existe y que la contraseña es correcta. Aquí aplican las políticas de bloqueo por intentos fallidos, y estas políticas deben vivir a nivel de tenant. El contador de intentos fallidos de jperez en el RFC ABC123 no afecta al jperez del RFC XYZ789.

Cuarto paso: Carga de entitlements. Con identidad verificada, cargas los claims que irán en el token: qué módulos tiene contratados el tenant, qué roles tiene el usuario, qué scopes aplican para el recurso solicitado. Este es el único momento en que consultas estos datos. El token los lleva encapsulados para evitar consultas repetidas.

Quinto paso: Emisión del par de tokens. Produces un access token de vida corta y un refresh token de vida larga.

Por qué el par access/refresh es la solución correcta

Un token de vida larga es conveniente —el usuario no se re-autentica seguido— pero peligroso —si se compromete, el atacante tiene acceso largo—. Un token de vida corta es seguro pero obliga a re-autenticación frecuente, que a su vez es insegura porque expone credenciales repetidamente.

La solución es separar la vida útil de uso de la vida útil de autenticación. El access token tiene vida corta —10 a 15 minutos— y se usa en cada request. Si se compromete, expira pronto. El refresh token tiene vida larga —días o semanas— y se usa raramente, solo para renovar el access token. Su exposición es mínima porque viaja solo al Authorization Server, nunca a los Resource Servers.

Esto crea una propiedad elegante: la revocación de acceso de un tenant moroso no requiere invalidación de tokens activos. Cuando el access token expira —máximo 15 minutos—, el cliente intenta refrescar usando el refresh token. En ese momento, el Authorization Server re-evalúa el estado del tenant. Si está suspendido por impago, el refresh es denegado y la sesión muere naturalmente. El sistema no necesita mecanismos de revocación activa para este caso.


La criptografía como arquitectura

La elección de algoritmo de firma no es solo un detalle de implementación. Determina la topología de confianza del sistema.

Firma simétrica vs. asimétrica

Con firma simétrica —un secreto compartido—, verificar y firmar son la misma operación con la misma clave. Esto significa que todo componente que necesite verificar tokens también puede firmarlos. El secreto debe distribuirse a todos los servicios. Cada servicio que lo conoce es un vector de ataque para falsificación de tokens.

Con firma asimétrica existe una separación criptográfica fundamental entre la capacidad de firmar y la capacidad de verificar. La clave privada —la capacidad de acuñar confianza— existe en exactamente un lugar. La clave pública —la capacidad de verificar confianza— puede distribuirse libremente sin riesgo.

Firma(m)       = Sign(m, k_priv)
Verificación(m, σ) = Verify(m, σ, k_pub)

La clave pública se publica en un endpoint estándar conocido como JWKS —JSON Web Key Set— en la ruta /.well-known/jwks.json. Cualquier servicio puede descargarlo, cachearlo localmente y verificar tokens completamente offline, sin ninguna llamada de red al Authorization Server en el camino crítico de cada request.

Rotación de claves sin downtime

Las claves criptográficas deben rotarse periódicamente. Si la clave privada se compromete, debes poder invalidarla sin interrumpir el servicio.

El mecanismo es el campo kid —Key ID— en el header del JWT. Cuando el Authorization Server firma un token, incluye en el header qué versión de la clave usó. El JWKS puede publicar múltiples claves públicas simultáneamente, cada una con su kid. Cuando un Resource Server recibe un token, lee el kid del header y usa la clave pública correspondiente para verificar.

Esto permite una rotación sin downtime: introduces la nueva clave en el JWKS —ahora hay dos—, empiezas a firmar tokens nuevos con la nueva clave, esperas que todos los tokens firmados con la clave vieja expiren, luego retiras la clave vieja del JWKS. En ningún momento hay un token válido que no pueda ser verificado.


Multi-tenancy: los patrones de aislamiento

El aislamiento multi-tenant tiene tres niveles que representan distintos tradeoffs entre costo operacional, aislamiento de seguridad y eficiencia de recursos.

Base de datos por tenant

Cada tenant tiene su propia base de datos completamente separada. El Authorization Server mantiene un catálogo que mapea cada RFC a su cadena de conexión.

Aislamiento total de datos. Una fuga de SQL injection en la BD del tenant A no puede exponer datos del tenant B. Esto es especialmente relevante en México para cumplimiento con datos fiscales —si un auditor del SAT pide los datos del RFC X, puedes exportar exactamente esa BD sin tocar los demás.

Costo operacional alto. Cada BD requiere su propio mantenimiento, backups, índices, migraciones. Con 100 tenants, una migración de esquema implica 100 operaciones separadas. Necesitas un motor de migraciones que opere sobre todas las BDs del catálogo en secuencia controlada, con capacidad de rollback por tenant.

El catálogo como componente crítico. Si el catálogo de tenants no está disponible, el Authorization Server no puede autenticar a nadie. Esto lo convierte en el componente de mayor disponibilidad requerida del sistema —mayor incluso que los Resource Servers, porque es la puerta de entrada a todo.

Aislamiento por esquema

Todos los tenants comparten el mismo servidor de base de datos, pero cada uno tiene su propio esquema. Reduce el costo operacional manteniendo cierto aislamiento lógico. Las migraciones son más simples —un solo servidor— pero el aislamiento físico desaparece. Una carga de un tenant puede afectar el rendimiento de otro.

Tabla compartida con discriminador

Todos los tenants comparten tablas, diferenciados por un tenant_id en cada fila. El costo operacional es mínimo pero el aislamiento es completamente lógico. Requiere disciplina absoluta en cada query para incluir el tenant_id en los filtros. Un bug aquí expone datos cross-tenant.

Con RFC como identificador y datos fiscales involucrados, el modelo de BD por tenant es la elección correcta desde perspectivas regulatorias y de confianza del cliente. El costo operacional adicional es el precio del aislamiento real.


La propagación de identidad entre servicios

Cuando el Servicio A necesita llamar al Servicio B en nombre del usuario, la identidad del usuario original debe propagarse de forma segura a través de todo el grafo de servicios.

Token forwarding: el caso simple

El Servicio A simplemente reenvía el token original del usuario. Esto funciona si el Servicio B aparece en la lista aud —audience— del token. La precondición es que el token fue emitido con la audiencia correcta. Un token con aud: ["servicio-facturacion"] no puede usarse en el servicio de contabilidad. Si se intercepta, es inútil fuera de su scope.

Token exchange: delegación limpia

Hay escenarios donde el Servicio A necesita llamar al Servicio B pero no debería reenviar el token del usuario. Quizás B necesita un token más acotado, con menos permisos, o quizás A necesita operar con cierta autonomía.

El patrón es que A intercambia el token del usuario por un nuevo token acotado, presentando al Authorization Server el token original como credencial. El nuevo token puede tener menos claims, audiencia restringida, o vida útil diferente. El Servicio B sabe que el request viene de A actuando en nombre del usuario original porque el nuevo token incluye un claim de actor —act— que representa la cadena de delegación.

Client credentials: comunicación máquina a máquina

Cuando no hay usuario involucrado —un proceso batch, una tarea programada, un servicio que hace mantenimiento— se usa el flujo de client credentials. El servicio tiene su propia identidad —un client_id y client_secret, o mejor aún, un certificado mTLS— y solicita tokens directamente al Authorization Server sin representar a ningún usuario.


Los límites como ciudadanos de primer orden

Hay tres tipos de límites que se confunden frecuentemente y requieren mecanismos completamente diferentes.

Límites de entitlement: puedes hacer esto

Son los módulos contratados, los permisos, los roles. Son datos relacionados con el plan y raramente cambian. Viven en el token como claims porque son del grano justo para ello: cambian cuando el tenant actualiza su plan o cuando un admin cambia el rol de un usuario, eventos infrecuentes.

La latencia de propagación aceptable aquí es el TTL del access token —15 minutos—. Si un admin revoca el acceso a un módulo, el usuario puede seguir usándolo hasta que su token expire. Es un tradeoff documentado y generalmente aceptable en sistemas empresariales B2B.

Límites de cuota: cuántas veces puedes hacer esto

Son los límites de uso: máximo N usuarios activos, máximo N documentos, N llamadas a la API por día. Son estado mutable de alta frecuencia. Nunca van en el token.

El patrón correcto es un contador atómico distribuido —Redis es el estándar— con una clave que incluya el tid, el tipo de recurso y el período de tiempo. Cada Resource Server, antes de procesar una operación, incrementa el contador y verifica si excede el límite del plan. Si excede, rechaza la operación con un error específico —no un 403 genérico—. El cliente necesita saber que es un problema de cuota, no de permisos.

La atomicidad es crítica. Si dos requests del mismo tenant llegan simultáneamente y ambos verifican el contador antes de incrementar, ambos pasarán aunque juntos exceden el límite. La operación de incrementar-y-verificar debe ser atómica.

Límites de rate limiting: velocidad de requests

Son diferentes a las cuotas. Una cuota es un límite acumulativo —N total en el período—. Rate limiting es una restricción de velocidad —N por segundo o minuto—. El mecanismo es similar —contadores en Redis con ventanas de tiempo— pero la semántica es diferente. Un tenant con cuota alta puede ser bloqueado por rate limiting si hace burst de requests. Protege al sistema de ser saturado por un solo tenant aunque técnicamente esté dentro de su cuota.


El estado del tenant como máquina de estados

La suscripción y estado de un tenant no es un booleano activo/inactivo. Es una máquina de estados con transiciones definidas y consecuencias arquitectónicas en cada estado.

Trial. El tenant existe, tiene credenciales, puede autenticarse. Acceso restringido a módulos básicos. Límites de uso reducidos. El Authorization Server incluye en el token un claim que indica el estado trial, y los Resource Servers pueden usarlo para mostrar banners de conversión o restringir funcionalidades premium.

Active. Estado nominal. El Authorization Server emite tokens con todos los módulos contratados. No hay restricciones adicionales más allá del plan.

Past due. La fecha de pago pasó pero hay un período de gracia. Las sesiones existentes siguen válidas. Los refresh tokens, al intentar renovar, pueden recibir un token degradado que restringe operaciones de escritura pero permite lectura. Es más elegante que cortar el acceso abruptamente.

Suspended. Período de gracia expirado. Los refresh tokens son rechazados. Los access tokens existentes continúan hasta expirar —máximo 15 minutos de exposición—. No se puede hacer login. El sistema continúa sirviendo datos en modo de solo lectura a nivel de acceso, para que el cliente pueda exportar sus datos, pero el Authorization Server no emite tokens nuevos.

Cancelled. La relación terminó. Las credenciales quedan archivadas —no eliminadas, datos fiscales tienen retención obligatoria en México—. La BD del tenant puede migrarse a almacenamiento frío.

La transición entre estados es responsabilidad de un componente separado —el sistema de billing o subscriptions— que actualiza el estado en el catálogo. El Authorization Server solo lee este estado; nunca lo modifica. Esta separación de responsabilidades permite que el sistema de billing evolucione independientemente.


El diseño del token: un contrato, no un contenedor

El JWT no es una base de datos portátil del usuario. Es un contrato de confianza con claims específicos que sirven propósitos específicos.

Claims estándar y su función

El claim iss —issuer— establece quién emitió el token. Los Resource Servers solo aceptan tokens de emisores conocidos. Si alguien genera un JWT arbitrario con los mismos claims pero sin la firma correcta, es rechazado.

El claim aud —audience— establece para quién es el token. Un token emitido para el servicio de facturación debe ser rechazado por el servicio de contabilidad aunque sea perfectamente válido. Esto limita el daño de la interceptación: un token robado del canal al servicio A no puede ser reproducido contra el servicio B.

El claim exp establece hasta cuándo es válido. Es el mecanismo de caducidad natural que hace que la revocación activa sea opcional para casos normales.

El claim jti —JWT ID— es un identificador único del token. Su función principal es servir de ancla para la deny-list en casos de revocación urgente.

Claims de tenant y el problema de la privacidad

El tid —RFC en tu caso— viaja en el token y es legible por cualquier Resource Server que reciba el token. Esto es intencional: los Resource Servers necesitan saber a qué tenant pertenece el request para resolver conexiones de BD y aplicar reglas de tenant.

Sin embargo, el token puede ser decodificado —base64— por cualquiera, incluyendo el usuario. Esto no compromete la seguridad —la integridad viene de la firma, no del secreto del contenido—, pero significa que no deberías poner en el token información que no quieras que el usuario vea, como detalles internos del plan o indicadores de riesgo de fraude.


Neutralidad de proveedor: identidad sin vendor lock-in

El problema de vendor lock-in en sistemas de identidad tiene dos dimensiones.

La primera es el proveedor de identidad. Si tu Authorization Server depende de APIs propietarias de Azure Entra ID, migrar a otro proveedor requiere reescribir toda la lógica de autenticación. La solución es que tu Authorization Server hable protocolos estándar —OIDC, OAuth2— como su contrato externo, sin importar qué usa internamente. Los Resource Servers apuntan al JWKS URL de tu Auth Server, no al de Azure. Si mañana cambias el backend del Auth Server, los Resource Servers no se enteran.

La segunda dimensión es el proveedor de infraestructura. Aquí aplica el patrón Hexagonal: tu dominio de autenticación define interfaces —puertos— para las operaciones que necesita. Almacenar una clave privada, verificar una contraseña, leer el estado de un tenant. Las implementaciones concretas —Key Vault de Azure, AWS KMS, HashiCorp Vault— son adaptadores que se enchufan a esos puertos. El dominio nunca importa el SDK de un proveedor específico.

La prueba de fuego de la neutralidad es: ¿puedo cambiar de Azure a AWS editando solo el archivo de configuración de qué adaptadores usar, sin tocar el dominio? Si la respuesta es no, hay acoplamiento indebido.

El protocolo OIDC como contrato es especialmente poderoso porque significa que cualquier cliente que funcione con Google Login, Auth0 o Okta funcionará con tu Authorization Server sin cambios. El contrato es el estándar, no tu implementación.


Observabilidad: la identidad necesita ser auditable

Un sistema de identidad sin observabilidad es una caja negra que nadie puede operar con confianza. Los eventos de identidad son también eventos de negocio y eventos de seguridad.

Cada intento de autenticación —exitoso o fallido— debe emitir un evento estructurado que incluya: timestamp, IP de origen, tenant —RFC—, usuario, resultado —success, failure, tenant_suspended, wrong_password—, user agent, y un request ID correlacionable. Estos eventos no van al log de aplicación genérico. Tienen su propia cadena de persistencia con retención larga —mínimo un año para datos fiscales en México—.

El patrón de correlación distribuida es que el jti del access token actúa como trace ID a través de todos los servicios. Cuando el Resource Server registra una operación, incluye el jti del token que autorizó esa operación. Esto permite, dado un incidente, reconstruir exactamente qué hizo un usuario específico con qué token en qué momento a través de qué servicios.


La jerarquía completa en reposo

Todas estas piezas forman una jerarquía de confianza donde cada nivel depende del anterior pero no al revés.

El almacén de claves —KMS o equivalente— es el nivel más bajo y más crítico. Es donde vive la clave privada. Su disponibilidad determina la disponibilidad del Authorization Server.

El Authorization Server depende del almacén de claves y del catálogo de tenants. Es el único que puede emitir tokens. Su disponibilidad determina la capacidad de los usuarios para iniciar nuevas sesiones, pero no afecta las sesiones activas. Los tokens ya emitidos siguen siendo válidos y verificables sin él.

Los Resource Servers dependen solo del JWKS cacheado localmente para verificar tokens. En caso de que el Authorization Server no esté disponible, las sesiones activas continúan funcionando. Solo los nuevos logins y los refreshes fallan. Esta degradación graceful es una propiedad arquitectónica deliberada: el sistema falla de la forma menos disruptiva posible.

El servicio de revocación —deny-list en Redis— es opcional en el camino crítico. Si Redis no está disponible, los Resource Servers pueden configurarse para operar sin él, aceptando el riesgo de no poder revocar tokens de emergencia, o para rechazar todos los tokens. Fail closed es más seguro pero más disruptivo.

Esta jerarquía, con sus dependencias explícitas y sus modos de degradación, es lo que separa un sistema de identidad de producción de uno que simplemente funciona en condiciones ideales.


La arquitectura de identidad no se trata de elegir entre JWT y sesiones, ni de cuántos minutos dura un token. Se trata de entender que la confianza es un recurso que se acuña, se verifica, se distribuye y se revoca. Cada componente del sistema debe saber exactamente cuál es su responsabilidad en esa cadena y no asumir ninguna más.