Type something to search...
Arquitectura de Base de Datos para Identidad, Autenticación y Autorización (IAM)

Arquitectura de Base de Datos para Identidad, Autenticación y Autorización (IAM)

Cuando se habla de seguridad en el contexto de una aplicación, la conversación casi siempre gira en torno a las capas visibles: el cifrado en tránsito, las políticas de contraseñas, los tokens de autenticación. Son piezas visibles e importantes, pero debajo de todas ellas existe una estructura que determina si un sistema IAM puede sostenerse en producción o si eventualmente colapsará bajo su propia complejidad. Esa estructura es el modelo de datos.

Diseñar una base de datos para gestionar identidad, autenticación y autorización no es simplemente crear una tabla de usuarios con nombre, email y contraseña. Es resolver, desde el nivel más fundamental, preguntas como: ¿quién es una entidad dentro del sistema? ¿cómo demuestra que es quien dice ser? ¿qué puede hacer? ¿a qué organización pertenece? Y hacerlo de una forma que no requiera rediseños costosos cuando la aplicación crezca de diez usuarios a diez millones.

Este artículo explora un modelo de datos completo para IAM, desde sus principios arquitectónicos más fundamentales hasta las decisiones de implementación que determinan si el sistema puede escalar, cumplir requisitos de seguridad en producción y adaptarse a entornos enterprise sin romperse en el proceso.


La separación entre identidad y autenticación

El punto de partida de todo el modelo es una decisión que parece obvia pero que la mayoría de las implementaciones no respetan: la identidad de una entidad y el mecanismo por el cual se autentica son dos cosas completamente distintas.

En la práctica, esto se traduce en separar el concepto de actor del concepto de cuenta de acceso. Un actor es cualquier entidad que existe en el sistema: una persona, una empresa cliente, un bot interno, un proveedor externo. Una cuenta de acceso es el mecanismo que permite a ese actor demostrarlo cuando lo necesita. Y la clave es que no todos los actores necesitan una cuenta.

Piense en el escenario de una plataforma SaaS donde un usuario registra una empresa como cliente. En ese momento la empresa existe como entidad en el sistema, tiene datos de contacto y puede ser referenciada desde otros registros. Pero quizás nadie en esa empresa necesita ingresar al sistema aún. Si el modelo obligara a crear una cuenta de autenticación cada vez que se crea una entidad, ese escenario sería imposible sin truismos como usuarios ficticios o campos nulos que van acumulando deuda técnica.

La tabla actor en el modelo es deliberadamente mínima: un identificador único, un tipo de entidad y un nombre. Todo lo demás se construye a partir de ahí.

CREATE TABLE actor (
    actor_id     UUIDv7       PRIMARY KEY,
    tipo_actor   UUIDv7       NOT NULL REFERENCES actor_tipo(tipo_id),
    nombre       VARCHAR(256) NOT NULL,
    eliminado_en TIMESTAMP    NULL
);

El campo eliminado_en merece una mención especial porque representa otra decisión fundamental: estas entidades nunca se eliminan físicamente en producción. En lugar de borrar un registro, se marca con una fecha lógica de eliminación. La razón es pragmática: la tabla de auditoría va a referenciar este actor durante años. Si el registro desapareciera, esa referencia estaría rota, y con ella cualquier intento de reconstruir qué sucedió y cuándo. Además, regulaciones como el GDPR exigen retención de datos por períodos definidos, lo cual es imposible si ya no existen.

Los datos específicos de cada tipo de actor se almacenan por separado, en tablas que se relacionan al actor mediante una clave foránea que es simultáneamente clave primaria. Así, la información de una persona natural —nombre, apellidos— vive en actor_persona, los documentos de identidad en actor_documento, y los contactos en tablas propias para correos, teléfonos y direcciones. Cada uno de estos, además, soporta múltiples valores con un campo de contexto que distingue entre un correo personal, uno laboral o uno de facturación, sin que la aplicación tenga que adivinar cuál usar según la circunstancia.


Por qué UUIDv7 y no otro identificador

Una decisión que atraviesa todo el modelo es el uso de UUIDv7 como identificador primario en todas las tablas principales. Es una elección que tiene consecuencias concretas en rendimiento y operación, no solo en diseño.

El problema con usar valores como el email o el número de documento como clave es simple: cambian. Un usuario puede cambiar su correo electrónico mañana. Si ese email era la clave que todos los otros registros usaban para referirlo, cada uno de ellos necesitaría actualizarse en cascada, con el riesgo de inconsistencias y la costosa operación de actualizar claves foráneas en decenas de tablas.

UUIDv4, el estándar más común, resuelve eso: genera identificadores únicos que no cambian. Pero UUIDv7 va un paso más allá. Sus primeros 48 bits contienen un timestamp del momento de creación. Eso tiene dos efectos inmediatos en la base de datos.

El primero es de rendimiento. Los índices B-tree, que son la estructura por defecto en la gran mayoría de bases de datos relacional, se mantienen ordenados por el valor de la clave. Con UUIDv7, ese orden es automáticamente cronológico. En una tabla como la de auditoría, que puede crecer a millones de filas por día, las consultas que buscan los eventos de las últimas 24 horas no necesitan un sort adicional: los datos ya están en ese orden físicamente.

El segundo es de operación diaria. Cuando un desarrollador ve un ID en un log de producción a las 3 de la mañana investigando un incidente, con UUIDv7 puede derivar aproximadamente cuándo fue creado ese registro sin tener que ir a la base de datos. Es un detalle pequeño, pero en las situaciones de mayor presión esos detalles marcan la diferencia entre resolver un problema en minutos o en horas.


La cuenta y sus credenciales

La cuenta de acceso (cuenta_acceso) es el puente entre un actor y el sistema de autenticación. Se crea únicamente cuando el actor necesita autenticarse, y contiene los campos que el sistema necesita para controlar el acceso en tiempo real: el estado de la cuenta, un contador de intentos fallidos consecutivos y una fecha de bloqueo automático.

CREATE TABLE cuenta_acceso (
    cuenta_id                      UUIDv7      PRIMARY KEY,
    actor_id                       UUIDv7      NOT NULL REFERENCES actor(actor_id),
    estado                         VARCHAR(20) NOT NULL DEFAULT 'activa',
    intentos_fallidos_consecutivos INT         NOT NULL DEFAULT 0,
    bloqueada_hasta                TIMESTAMP   NULL,
    creado_en                      TIMESTAMP   NOT NULL DEFAULT NOW(),
    eliminado_en                   TIMESTAMP   NULL
);

El diseño de los campos de lockout en esta tabla no es accidental. El contador intentos_fallidos_consecutivos se incrementa con cada fallo y se reinicia al cero en cada login exitoso. Cuando supera el umbral configurado por la organización —por defecto cinco intentos— el sistema calcula una fecha de bloqueo y la escribe en bloqueada_hasta. Desde ese momento, cualquier intento de login contra esa cuenta falla automáticamente hasta que la fecha pase. Este mecanismo es necesario para defender contra ataques de fuerza bruta, pero el umbral y la duración del bloqueo no están hardcoded: cada organización puede configurarlos de forma independiente según sus requisitos de seguridad.

Las credenciales reales viven en una tabla separada, cuenta_credencial, y aquí el modelo hace otra cosa interesante. Una sola cuenta puede tener varias credenciales simultáneamente. Un usuario puede entrar con contraseña local, conectar su cuenta de Google y usar SAML corporativo, todo bajo la misma cuenta. Cada credencial tiene un campo proveedor que indica de dónde viene —LOCAL, GOOGLE, SAML— y el sistema sabe cómo procesar cada una según ese valor.

CREATE TABLE cuenta_credencial (
    credencial_id   UUIDv7       PRIMARY KEY,
    cuenta_id       UUIDv7       NOT NULL REFERENCES cuenta_acceso(cuenta_id),
    proveedor       VARCHAR(40)  NOT NULL,
    identificador  VARCHAR(320) NOT NULL,
    secreto_hash    VARCHAR(256) NULL,
    algoritmo_hash  VARCHAR(40)  NULL,
    parametros_hash JSONB        NULL,
    creado_en       TIMESTAMP    NOT NULL DEFAULT NOW(),
    UNIQUE (cuenta_id, proveedor, identificador)
);

Para las credenciales locales, la contraseña se almacena como hash —nunca en texto plano— y junto a ella se guardan dos campos que habitualmente se olvidan: algoritmo_hash y parametros_hash. La razón de su existencia tiene que ver con un problema real que aparece cuando una aplicación madura. En el momento en que el sistema decide migrar de bcrypt a Argon2id —algo que eventualmente todo sistema serio debe hacer— no es posible invalidar todas las contraseñas de la base de datos de una vez. La solución es lo que se conoce como migración lazy: cuando un usuario hace login, si su hash fue generado con un algoritmo antiguo, el sistema lo regenera con el algoritmo actual sin pedir que el usuario cambie su contraseña. Para que eso funcione, el sistema necesita saber exactamente qué algoritmo y qué parámetros usó para generar cada hash. De ahí vienen esos dos campos.


Sesiones, tokens y el problema de la revocación

Una vez que un usuario se autentica exitosamente, el sistema crea una sesión. La sesión es el registro que representa una instancia activa de uso desde un dispositivo específico, y es el lugar donde vive una de las decisiones más importantes del modelo desde el punto de vista de seguridad.

CREATE TABLE cuenta_sesion (
    sesion_id      UUIDv7      PRIMARY KEY,
    cuenta_id      UUIDv7      NOT NULL REFERENCES cuenta_acceso(cuenta_id),
    version        BIGINT      NOT NULL DEFAULT 1,
    dispositivo_id UUIDv7      NULL REFERENCES cuenta_dispositivo(dispositivo_id),
    creada_en      TIMESTAMP   NOT NULL DEFAULT NOW(),
    expira_en      TIMESTAMP   NOT NULL,
    ip             INET        NOT NULL,
    user_agent     TEXT        NOT NULL
);

El campo version es el que hace la diferencia. En un sistema típico que usa JWT —JSON Web Tokens— como mecanismo de autenticación por token, cada access token tiene una duración fija, por ejemplo quince minutos. Si un token es robado, el sistema no puede invalidarlo antes de que llegue a expirar: el token es autosuficiente por diseño. Durante esos quince minutos, el token robado es completamente válido. En una cuenta que ha sido comprometida, quince minutos pueden ser más que suficientes para causas daños significativos.

La solución que implementa este modelo es elegante en su simplicidad. Cada access token emitido incluye la versión actual de la sesión en su payload. Cuando el usuario realiza una acción que debe invalidar sus tokens —cambiar contraseña, habilitar MFA, cerrar sesión remota— el sistema simplemente incrementa la versión en la base de datos. El middleware de autenticación compara la versión del token con la versión almacenada. Si no coinciden, el token es rechazado inmediatamente, en milisegundos, sin esperar a que expiré.

Los refresh tokens operan en un esquema complementario. Son de duración larga —hasta treinta días— y su propósito es permitir obtener nuevos access tokens sin que el usuario vuelva a ingresar sus credenciales. La tabla cuenta_refresh_token incluye un campo que vale la pena destacar: reemplazado_por. Cuando un refresh token se rota —lo cual debe ocurrir cada vez que se genera un nuevo access token— el antiguo apunta al nuevo mediante ese campo. Esto crea una cadena auditable, pero más importante, permite detectar ataques. Si alguien roba un refresh token y lo usa después de que ya fue rotado, el sistema detecta que el token de la cadena ya fue reemplazado y puede revocar toda la sesión.


Protección contra ataques y el arte de no revelar demasiado

El control de intentos de login es donde la seguridad se enfrenta directamente con la experiencia del usuario, y donde las decisiones de diseño en la base de datos tienen consecuencias de seguridad reales.

CREATE TABLE cuenta_intento_login (
    intento_id     UUIDv7       PRIMARY KEY,
    cuenta_id      UUIDv7       NULL REFERENCES cuenta_acceso(cuenta_id),
    identificador  VARCHAR(320) NOT NULL,
    exito          BOOLEAN      NOT NULL,
    ip             INET         NOT NULL,
    user_agent     TEXT         NOT NULL,
    motivo_fallo   VARCHAR(64)  NULL,
    creado_en      TIMESTAMP    NOT NULL DEFAULT NOW()
);

El detalle más sutil de esta tabla es que cuenta_id es nulable. Cuando alguien intenta hacer login con un email que no existe en el sistema, el registro se crea con cuenta_id = NULL. La razón tiene que ver con un ataque conocido como enumeración de usuarios: si el sistema respondiera de forma diferente ante un email no registrado versus una contraseña incorrecta, un atacante podría ir probando emails hasta confirmar cuáles están registrados. Al registrar todos los intentos de forma uniforme, independientemente de si la cuenta existe o no, y al no distinguir entre tipos de error en la respuesta al cliente, el sistema cierra esa puerta.

El campo motivo_fallo almacena la información detallada del fallo, pero esta información es para uso interno exclusivamente. El sistema nunca la retorna al cliente: desde afuera, un fallo es simplemente un fallo, sin distingos. Es un patrón que aparece repetido en varios lugares del modelo, esta idea de que hay información que el sistema necesita almacenar para su propia operación pero que no debe revelar hacia afuera.

El lockout funciona en dos niveles independientes. El primero es por cuenta: cuando los intentos fallidos consecutivos supera el umbral, la cuenta se bloquea por un período configurable. El segundo es por IP: si una misma dirección IP genera demasiados intentos fallidos contra cuentas diferentes en un período corto, se activa rate limiting a nivel de red. Ese segundo nivel es el que detecta credential stuffing, uno de los ataques más comunes hoy.


Políticas de contraseña como modelo de datos

Las políticas de contraseñas en la mayoría de las aplicaciones se implementan como constantes en el código: mínimo ocho caracteres, debe tener mayúscula, debe tener número. El problema con ese enfoque es que es imposible auditorlo, imposible que cada organización tenga requisitos diferentes, y requiere un deploy cada vez que cambie una regla.

En este modelo las políticas son una tabla en sí misma, con un campo tenant_id nulable que determina su alcance. Si es NULL, es la política global que aplica a todos; si tiene valor, es específica de esa organización y tiene prioridad.

CREATE TABLE politica_password (
    politica_id         UUIDv7      PRIMARY KEY,
    tenant_id           UUIDv7      NULL REFERENCES tenant(tenant_id),
    min_longitud        INT         NOT NULL DEFAULT 12,
    max_longitud        INT         NOT NULL DEFAULT 128,
    requiere_mayuscula  BOOLEAN     NOT NULL DEFAULT TRUE,
    requiere_minuscula  BOOLEAN     NOT NULL DEFAULT TRUE,
    requiere_numeros    BOOLEAN     NOT NULL DEFAULT TRUE,
    requiere_especiales BOOLEAN     NOT NULL DEFAULT TRUE,
    max_edad_dias       INT         NULL,
    historial_prohibido INT         NOT NULL DEFAULT 5,
    creado_en           TIMESTAMP   NOT NULL DEFAULT NOW()
);

El campo historial_prohibido merece explicación porque implica la existencia de otra tabla: cuenta_historial_password. Cuando un usuario cambia su contraseña, el sistema necesita verificar que la nueva no coincida con las últimas N que tuvo. Para poder hacer esa comparación, los hashes de las contraseñas anteriores deben estar almacenados en algún lugar. En esa tabla, junto con cada hash antiguo se guardan también el algoritmo y los parámetros que fueron usados para generarlo, por la misma razón de compatibilidad que ya mencionamos: el algoritmo puede haber cambiado entre cuando se creó ese hash y el momento en que se necesita compararlo.

El límite máximo de longitud, que a primera vista parece arbitrario, tiene una razón de seguridad: una contraseña extremadamente larga puede usarse para un ataque de denegación de servicio, ya que calcular el hash de una cadena de miles de caracteres consume recursos significativos.


Multi-tenancy: aislamiento sin multiplicar la base de datos

Cuando una aplicación necesita servir a múltiples organizaciones independientes, la tentación es crear una base de datos separada por cada una. Es la solución más aislada, pero también la más costosa en operación: N bases de datos significan N planes de backup, N procesos de monitoreo, N niveles de mantenimiento.

El modelo propone la alternativa estándar en la industria: una sola base de datos compartida donde cada organización —llamada tenant— tiene sus datos lógicamente aislados mediante una clave tenant_id que atraviesa todas las tablas relevantes.

CREATE TABLE tenant (
    tenant_id     UUIDv7       PRIMARY KEY,
    nombre        VARCHAR(256) NOT NULL,
    estado        VARCHAR(20)  NOT NULL DEFAULT 'activo',
    plan          VARCHAR(64)  NULL,
    configuracion JSONB        NULL,
    creado_en     TIMESTAMP    NOT NULL DEFAULT NOW(),
    eliminado_en  TIMESTAMP    NULL
);

Un actor puede ser miembro de múltiples tenants simultáneamente, lo cual refleja la realidad de consultores o profesionales que trabajan con varias empresas. La membresía se gestiona en tenant_miembro, y las invitaciones en tenant_invitacion, donde tanto el email del invitado como el token de validación se almacenan como hash. Esto evita que alguien con acceso directo a la base de datos pueda enumerar qué emails tienen invitaciones pendientes en cada organización, un vector de ataque que suena teórico hasta que alguien lo explota.

Cada organización puede además tener sus propios requisitos de seguridad: cuántos intentos fallidos se permiten antes del lockout, si el MFA es obligatorio, qué métodos de MFA están permitidos, cuál es la política de contraseñas. Todas estas configuraciones se almacenan en tablas de configuración por tenant, lo cual significa que una organización enterprise puede exigir FIDO2 como único método de MFA mientras otra más pequeña se conforma con TOTP, sin que eso afecte al resto del sistema.


Autorización por roles y grupos

El módulo de autorización implementa RBAC, Role-Based Access Control, con una extensión importante: soporte para grupos organizacionales. La estructura básica es la clásica: roles que contienen permisos, y usuarios que tienen roles asignados. Pero la realidad de las organizaciones grandes no funciona así de simple.

En una empresa real, los permisos no se asignan uno por uno a cada persona. Se asignan por departamento, por unidad organizacional. Cuando un nuevo empleado entra al área de finanzas, debería recibir automáticamente los permisos que corresponden a esa función, sin que un administrador los configure manualmente uno por uno.

Para resolver eso, el modelo incluye una capa de grupos dentro de cada tenant. Un grupo como “Finanzas” tiene asignado el rol CONTADOR. Cuando alguien se agrega al grupo, hereda automáticamente todos los permisos de ese rol. Si además necesita algo extra —un permiso que no aplica a todo el grupo— se le asigna un rol adicional a nivel individual mediante la tabla miembro_rol. Las dos vías coexisten sin conflicto.

CREATE TABLE tenant_grupo (
    grupo_id    UUIDv7       PRIMARY KEY,
    tenant_id   UUIDv7       NOT NULL REFERENCES tenant(tenant_id),
    nombre      VARCHAR(128) NOT NULL,
    descripcion VARCHAR(256) NULL,
    UNIQUE (tenant_id, nombre)
);

La restricción UNIQUE (tenant_id, nombre) es un detalle que evita un error de diseño frecuente: que dos grupos con el mismo nombre existan en la misma organización, lo cual generaría confusión tanto en la interfaz administrativa como en la lógica de asignación.


Delegación temporal y dispositivos confiables

Hay dos escenarios frecuentes en organizaciones que RBAC por sí solo no resuelve. El primero es la delegación temporal: un manager que se va de vacaciones y necesita que alguien apruebe en su lugar durante dos semanas. El segundo es la experiencia del usuario en sistemas con MFA: si ya completaste la verificación en tu laptop esta mañana, no deberías tener que hacerlo de nuevo cada quince minutos.

La delegación se implementa mediante delegacion_permiso, una tabla que tiene una fecha de inicio y una fecha de fin obligatoria. No existen delegaciones perpetuas en el modelo, y es una decisión consciente: cualquier delegación sin límite temporal es un riesgo de seguridad que eventualmente alguien olvidará revocar. La delegación puede ser un rol completo —“durante mis vacaciones, esta persona actúa como aprobador”— o permisos individuales —“solo necesita firmar esta factura específica”.

Los dispositivos confiables funcionan con un concepto de fingerprint: una combinación de browser, sistema operativo y otros atributos del dispositivo, almacenada como hash. Cuando un usuario completa MFA exitosamente en un dispositivo y acepta marcarlo como confiable, el sistema crea un registro con una fecha de expiración de la confianza —configurada por tenant. En logins futuros desde ese mismo dispositivo, si la confianza aún no ha expirado, el paso de MFA se omite automáticamente.

CREATE TABLE cuenta_dispositivo (
    dispositivo_id  UUIDv7       PRIMARY KEY,
    cuenta_id       UUIDv7       NOT NULL REFERENCES cuenta_acceso(cuenta_id),
    fingerprint     VARCHAR(256) NOT NULL,
    nombre          VARCHAR(128) NULL,
    confiable       BOOLEAN      NOT NULL DEFAULT FALSE,
    confiable_hasta TIMESTAMP    NULL,
    creado_en       TIMESTAMP    NOT NULL DEFAULT NOW(),
    ultimo_uso_en   TIMESTAMP    NOT NULL DEFAULT NOW(),
    UNIQUE (cuenta_id, fingerprint)
);

El campo ultimo_uso_en tiene un propósito de seguridad que no es inmediatamente obvio: permite mostrar al usuario cuándo fue usado cada dispositivo registrado, lo cual es la mecanismo por el cual un usuario puede detectar que alguien más está usando su cuenta desde un dispositivo que no reconoce.


Extensiones enterprise: SSO, SCIM y ABAC

Cuando una aplicación necesita integrarse con el ecosistema de identidad corporativo existente —Active Directory, Okta, Azure AD— el modelo incluye las tablas necesarias para eso sin rediseñar lo que ya existe.

SSO se implementa mediante cuenta_identidad_externa, que vincula una cuenta del sistema con un identificador externo proporcionado por el proveedor de identidad corporativo. SCIM, el estándar de provisioning automático, agrega otra dimensión: cuando un empleado se crea o elimina en el directorio corporativo, los cambios se reflejan automáticamente en el sistema. La tabla scim_provisioning registra el estado de cada usuario sincronizado y sus metadatos, los cuales pueden ser usados por las políticas ABAC.

ABAC —Attribute-Based Access Control— es la extensión más potente del sistema de autorización. En lugar de depender únicamente de roles estáticos, permite definir políticas basadas en atributos dinámicos tanto del usuario como del recurso.

CREATE TABLE politica_abac (
    politica_id UUIDv7       PRIMARY KEY,
    tenant_id   UUIDv7       NULL REFERENCES tenant(tenant_id),
    nombre      VARCHAR(128) NOT NULL,
    expresion   TEXT         NOT NULL,
    efecto      VARCHAR(10)  NOT NULL,
    activa      BOOLEAN      NOT NULL DEFAULT TRUE,
    creado_en   TIMESTAMP    NOT NULL DEFAULT NOW()
);

Una política ABAC puede expresar reglas como “permitir el acceso si el departamento del usuario coincide con el departamento del recurso y el nivel del usuario es mayor o igual a 2”. El campo expresion debe estar en un lenguaje controlado evaluado por un motor dedicado —como OPA con Rego— y no como texto libre, precisamente para evitar que esas expresiones sean vectores de inyección y para poder testarlas de forma aislada antes de ponerlas en producción.


Auditoría: el registro que no puede faltar

La tabla de auditoría es el componente que conecta todo el modelo con los requisitos de compliance. Cada evento de seguridad que ocurre en el sistema —login, logout, cambio de contraseña, creación de usuario, modificación de roles— se registra aquí con el actor que realizó la acción, el actor afectado si es diferente, el tenant en cuyo contexto ocurrió, y metadatos adicionales.

CREATE TABLE auditoria_seguridad (
    evento_id         UUIDv7       PRIMARY KEY,
    actor_id          UUIDv7       NOT NULL REFERENCES actor(actor_id),
    actor_afectado_id UUIDv7       NULL REFERENCES actor(actor_id),
    tenant_id         UUIDv7       NULL REFERENCES tenant(tenant_id),
    accion            VARCHAR(64)  NOT NULL,
    objeto_tipo       VARCHAR(64)  NULL,
    objeto_id         VARCHAR(256) NULL,
    fecha             TIMESTAMP    NOT NULL DEFAULT NOW(),
    ip                INET         NULL,
    user_agent        TEXT         NULL,
    metadata          JSONB        NULL
) PARTITION BY RANGE (fecha);

El detalle más importante desde el punto de vista de operación es la última línea: PARTITION BY RANGE (fecha). Esta tabla puede crecer a millones de filas por día en un sistema de actividad moderada. Sin partitioning, las consultas de auditoría se convierten en sequential scans que se vuelven más lentas semana tras semana hasta que el sistema necesita una intervención de emergencia. Con partitioning por mes, cada consulta solo escanea la partición relevante. Las particiones antiguas —más de doce meses— se archivan a almacenamiento frío y se eliminan de la base activa, manteniendo el tamaño operacional manejable.


Cifrado y clasificación de datos

No todos los datos requieren el mismo nivel de protección, y el modelo reconoce eso con una clasificación en cuatro niveles. Los datos críticos como hashes de contraseñas y tokens nunca se almacenan en texto plano. Los datos de identidad personal —email, teléfono, documento— se cifran en reposo a nivel de columna mediante cifrado a nivel de aplicación con AES-256-GCM, con las claves de cifrado almacenadas en un KMS externo, no en la base de datos.

Para campos PII que necesitan ser buscables, como el email, el modelo propone almacenar junto al valor cifrado un hash determinístico separado. El hash permite hacer búsquedas —“¿existe un usuario con este email?”— sin que la base de datos contenga el email en texto plano. Es un patrón que aparece también en las invitaciones y en los tokens de refresh.


Índices estratégicos

Los índices son donde el modelo pasa de ser un diseño teórico a ser un sistema que puede operar en producción. Las queries más frecuentes en un sistema IAM son predecibles: login, verificación de permisos, búsqueda de sesiones activas, consultas de auditoría. Sin los índices correctos para cada una de estas operaciones, cada request adicional de un usuario se convierte en un scan secuencial que crece linealmente con el tamaño de la tabla.

-- Login: el camino crítico de cada autenticación
CREATE UNIQUE INDEX idx_credencial_proveedor_identificador
    ON cuenta_credencial (proveedor, identificador);

-- Sesiones: buscar y limpiar sesiones expiradas
CREATE INDEX idx_sesion_cuenta_expira
    ON cuenta_sesion (cuenta_id, expira_en);

-- Intentos: detectar ataques por IP (los más recientes primero)
CREATE INDEX idx_intento_ip_fecha
    ON cuenta_intento_login (ip, creado_en DESC);

-- Auditoría: queries de compliance por actor
CREATE INDEX idx_auditoria_actor_fecha
    ON auditoria_seguridad (actor_id, fecha DESC);

-- Auditoría: queries de compliance por tenant
CREATE INDEX idx_auditoria_tenant_fecha
    ON auditoria_seguridad (tenant_id, fecha DESC);

El orden de las columnas en los índices compuestos no es arbitrario. En un índice como (tenant_id, actor_id), la primera columna debe ser la que aparece más frecuentemente en las condiciones de búsqueda. Como las queries siempre filtran por organización antes de filtrar por usuario, tenant_id va primero. Los índices DESC en columnas de fecha evitan que la base de datos necesite ordenar los resultados después de buscarlos cuando la query busca los últimos N registros.


Crecimiento progresivo sin rediseños

Una de las frustraciones más comunes al diseñar sistemas IAM es que la arquitectura inicial no anticipa la complejidad futura y eventualmente requiere rediseños que costo tiempo y dinero. Este modelo resuelve esa fricción con una estratificación por niveles de implementación.

El nivel más básico —login con email y contraseña— requiere apenas las tablas de actor, cuenta, credencial, intentos y política de contraseñas. Es suficiente para una aplicación interna pequeña. A partir de ahí, cada nivel agrega las tablas correspondientes sin modificar las existentes: sesiones y MFA para aplicaciones modernas, roles para paneles administrativos, tenants y grupos para plataformas SaaS, y finalmente SSO, SCIM, ABAC y OAuth para entornos enterprise.

Esta progresión no es solo teórica. Cada tabla en el modelo fue diseñada con las relaciones futuras en mente, de modo que agregar el nivel siguiente es una operación aditiva, no una refactorización. La clave que habilita eso es la separación original entre actor y cuenta, y el uso de identificadores inmutables que no cambian independientemente de cuántas tablas se agreguen después.


Líneas futuras y consideraciones de madurez

El modelo que se describe aquí cubre las necesidades de la gran mayoría de aplicaciones desde un login básico hasta un sistema enterprise. Sin embargo, existen áreas donde la complejidad puede crecer más allá de lo que un modelo relacional estándar puede manejar eficientemente.

La primera es la verificación de permisos en tiempo real en sistemas de alta concurrencia. Cuando la cantidad de roles, grupos y políticas ABAC crecen significativamente, la consulta que determina si un actor puede realizar una acción puede volverse costosa. Una línea de investigación viable es implementar una capa de caché —Redis o similar— que almacene las resoluciones de permisos por actor y se invalide cuando ocurren cambios en roles o políticas. La tabla de auditoría puede ser el trigger para esa invalidación.

La segunda área es la escalabilidad horizontal de la tabla de auditoría. Aunque el partitioning por fecha resuelve el problema de crecimiento a mediano plazo, en sistemas con millones de eventos diarios puede ser necesario considerar la migración de datos históricos a un almacenamiento analítico separado —un data warehouse o un sistema de búsqueda como Elasticsearch— mientras que la base relacional retiene solo los datos activos necesarios para la operación.

Finalmente, la evolución hacia modelos de identidad decentralizada —DID, credenciales verificables— es una dirección que la industria está explorando activamente. El modelo actual, al separar la identidad del mecanismo de autenticación desde el principio, tiene la estructura necesaria para adaptarse a esos cambios sin rediseño fundamental. Es otra prueba de que las decisiones arquitectónicas correctas no solo resuelven los problemas de hoy, sino que reducen la fricción de los cambios que vienen después.

Related Posts

Cuándo Usar Colas de Mensajes en el Desarrollo de Software

Cuándo Usar Colas de Mensajes en el Desarrollo de Software

Las colas de mensajes son herramientas clave para construir sistemas distribuidos, escalables y tolerantes a fallos. En este artículo te comparto una guía con situaciones comunes donde su uso es altam

Leer más
RabbitMQ 2: Arquitectura y Enrutamiento Avanzado en RabbitMQ

RabbitMQ 2: Arquitectura y Enrutamiento Avanzado en RabbitMQ

En nuestro primer artículo, exploramos qué es RabbitMQ, por qué es fundamental para la comunicación asíncrona en sistemas distribuidos y cuáles son sus casos de uso típicos. Lo comparamos con una "ofi

Leer más
RabbitMQ 1: Introducción a RabbitMQ, El Corazón de la Mensajería Asíncrona

RabbitMQ 1: Introducción a RabbitMQ, El Corazón de la Mensajería Asíncrona

En el mundo del desarrollo de software moderno, especialmente con el auge de los microservicios y los sistemas distribuidos, la forma en que las diferentes partes de una aplicación se comunican es fun

Leer más
RabbitMQ 3: Configuración y Gestión de Colas en RabbitMQ

RabbitMQ 3: Configuración y Gestión de Colas en RabbitMQ

Después de entender qué es RabbitMQ y cómo sus Exchanges y Bindings dirigen los mensajes, llegamos a la Cola. La cola es fundamentalmente un buffer confiable: es el lugar donde los mensajes esperan su

Leer más
RabbitMQ 4: Robustez y Seguridad en RabbitMQ

RabbitMQ 4: Robustez y Seguridad en RabbitMQ

Hemos recorrido el camino desde la introducción a RabbitMQ y su papel en la mensajería asíncrona, pasando por su arquitectura, componentes de enrutamiento (Exchanges y Bindings), y la gestión detallad

Leer más
RabbitMQ 5: Consumo de Recursos, Latencia y Monitorización de RabbitMQ

RabbitMQ 5: Consumo de Recursos, Latencia y Monitorización de RabbitMQ

Hemos explorado la teoría detrás de RabbitMQ, su arquitectura, cómo enruta mensajes y cómo podemos construir sistemas robustos y seguros. Sin embargo, para operar RabbitMQ de manera efectiva en produc

Leer más
RabbitMQ 6: Alta Disponibilidad y Escalabilidad con Clustering en RabbitMQ

RabbitMQ 6: Alta Disponibilidad y Escalabilidad con Clustering en RabbitMQ

Hasta ahora, hemos hablado de cómo un nodo individual de RabbitMQ maneja mensajes, gestiona colas, y cómo monitorizar su rendimiento y seguridad. Sin embargo, para aplicaciones críticas que no pueden

Leer más
Kafka 1: Introducción a Apache Kafka, fundamentos y Casos de Uso

Kafka 1: Introducción a Apache Kafka, fundamentos y Casos de Uso

En el panorama tecnológico actual, los datos son el motor que impulsa la innovación. La capacidad de procesar, reaccionar y mover grandes volúmenes de datos en tiempo real se ha convertido en una nece

Leer más
Kafka 2: Arquitectura Profunda de Kafka, Topics, Particiones y Brokers

Kafka 2: Arquitectura Profunda de Kafka, Topics, Particiones y Brokers

En nuestro primer artículo, despegamos en el mundo de Apache Kafka, sentando las bases de lo que es esta potente plataforma de streaming de eventos y diferenciándola de los sistemas de mensajería trad

Leer más
Kafka 3: Productores y Consumidores, Configuración y Buenas Prácticas

Kafka 3: Productores y Consumidores, Configuración y Buenas Prácticas

Hemos navegado por los conceptos esenciales de Apache Kafka y desentrañado la arquitectura que reside bajo la superficie, comprendiendo cómo los Topics se dividen en Particiones distribuidas entre Bro

Leer más
Kafka 4: Procesamiento de Datos en Tiempo Real con Kafka Streams y ksqlDB

Kafka 4: Procesamiento de Datos en Tiempo Real con Kafka Streams y ksqlDB

En los artículos anteriores, hemos construido una sólida comprensión de Apache Kafka: qué es, por qué es una plataforma líder para streaming de eventos, cómo está estructurado internamente con Topic

Leer más
Spring WebFlux 1: Fundamentos Reactivos y el Corazón de Reactor

Spring WebFlux 1: Fundamentos Reactivos y el Corazón de Reactor

¡Hola, entusiasta del desarrollo moderno! 👋 En el vertiginoso mundo de las aplicaciones web, donde la escalabilidad y la eficiencia son reyes, ha surgido un paradigma que desafía el modelo tradicion

Leer más
Spring WebFlux 2: Alta Concurrencia sin Más Hilos

Spring WebFlux 2: Alta Concurrencia sin Más Hilos

¡Bienvenido de nuevo a nuestra inmersión en Spring WebFlux! 👋 En la primera parte de esta serie, exploramos el "por qué" de la programación reactiva, entendiendo los problemas del bloqueo y descubri

Leer más
Kafka 6: Despliegue, Seguridad y Optimización

Kafka 6: Despliegue, Seguridad y Optimización

Hemos explorado la arquitectura fundamental de Apache Kafka, la dinámica entre productores y consumidores, sus potentes capacidades para el procesamiento de flujos de datos y las herramientas que enri

Leer más
Spring WebFlux 3: Comunicación, Datos y Errores Reactivos

Spring WebFlux 3: Comunicación, Datos y Errores Reactivos

¡Continuemos nuestro viaje por el fascinante mundo de Spring WebFlux! En la Parte 1, sentamos las bases de la programación reactiva y exploramos Project Reactor, el corazón de WebFlux. En la **Pa

Leer más
Kafka 7: Patrones Avanzados y Anti-Patrones con Kafka

Kafka 7: Patrones Avanzados y Anti-Patrones con Kafka

Hemos recorrido un camino considerable en nuestra serie sobre Apache Kafka. Desde sus fundamentos y arquitectura interna hasta la interacción con productores y consumidores, las herramientas de proces

Leer más
Kafka 5: Más Allá del Core, Explorando el Ecosistema de Apache Kafka

Kafka 5: Más Allá del Core, Explorando el Ecosistema de Apache Kafka

Hemos navegado por las entrañas de Apache Kafka, comprendiendo su funcionamiento interno, la interacción entre productores y consumidores, e incluso cómo procesar datos en tiempo real con Kafka Stream

Leer más
Spring WebFlux 4: Comunicación Avanzada, Pruebas y Producción

Spring WebFlux 4: Comunicación Avanzada, Pruebas y Producción

La serie Spring WebFlux nos ha llevado a través de un viaje fascinante por el mundo de la programación reactiva, desde sus fundamentos y el poder de Project Reactor hasta la construcción de arquit

Leer más
Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

Arquitectura DDD y Hexagonal: Construyendo Software para el Futuro

En el dinámico mundo del desarrollo de software, la complejidad es el enemigo silencioso. Las aplicaciones crecen, los requisitos cambian y, sin una guía clara, el código puede convertirse rápidamente

Leer más