# DDL-SPEC: Schema core_auth ## Identificacion | Campo | Valor | |-------|-------| | **Schema** | core_auth | | **Modulo** | MGN-001 | | **Version** | 1.0 | | **Estado** | En Diseño | | **Autor** | System | | **Fecha** | 2025-12-05 | --- ## Descripcion General El schema `core_auth` contiene todas las tablas relacionadas con autenticación, manejo de sesiones, tokens y recuperación de contraseñas. Es el schema más crítico desde el punto de vista de seguridad. ### Alcance - Gestión de tokens JWT (refresh tokens) - Control de sesiones activas - Historial de login/logout - Recuperación de contraseñas - Blacklist de tokens revocados > **Nota:** La tabla `users` reside en `core_users`, no en este schema. --- ## Diagrama Entidad-Relacion ```mermaid erDiagram %% Entidades externas (referencia) users ||--o{ refresh_tokens : "tiene" users ||--o{ session_history : "registra" users ||--o{ login_attempts : "intenta" users ||--o{ password_reset_tokens : "solicita" users ||--o{ password_history : "historial" refresh_tokens { uuid id PK uuid user_id FK uuid tenant_id FK varchar token_hash uuid family_id boolean is_used timestamptz used_at uuid replaced_by FK varchar device_info inet ip_address timestamptz expires_at timestamptz revoked_at varchar revoked_reason timestamptz created_at } revoked_tokens { uuid id PK varchar jti UK uuid user_id FK uuid tenant_id FK varchar token_type timestamptz original_exp varchar revocation_reason timestamptz revoked_at timestamptz created_at } session_history { uuid id PK uuid user_id FK uuid tenant_id FK varchar action varchar device_info inet ip_address jsonb metadata timestamptz created_at } login_attempts { uuid id PK varchar email uuid tenant_id FK boolean success varchar failure_reason inet ip_address varchar user_agent timestamptz attempted_at } password_reset_tokens { uuid id PK uuid user_id FK uuid tenant_id FK varchar token_hash integer attempts timestamptz expires_at timestamptz used_at timestamptz created_at } password_history { uuid id PK uuid user_id FK uuid tenant_id FK varchar password_hash timestamptz created_at } ``` --- ## Tablas ### 1. refresh_tokens Almacena los refresh tokens emitidos para mantener sesiones activas. #### Estructura | Columna | Tipo | Nullable | Default | Descripcion | |---------|------|----------|---------|-------------| | `id` | UUID | NOT NULL | gen_random_uuid() | PK | | `user_id` | UUID | NOT NULL | - | FK → core_users.users | | `tenant_id` | UUID | NOT NULL | - | FK → core_tenants.tenants | | `jti` | VARCHAR(64) | NOT NULL | - | JWT ID (unique identifier) | | `token_hash` | VARCHAR(255) | NOT NULL | - | Hash bcrypt del token | | `family_id` | UUID | NOT NULL | - | Familia de tokens (para rotación) | | `is_used` | BOOLEAN | NOT NULL | false | Si el token ya fue usado | | `used_at` | TIMESTAMPTZ | NULL | - | Cuando se usó para refresh | | `replaced_by` | UUID | NULL | - | Token que lo reemplazó | | `device_info` | VARCHAR(500) | NULL | - | User-Agent del dispositivo | | `ip_address` | INET | NULL | - | IP desde donde se creó | | `expires_at` | TIMESTAMPTZ | NOT NULL | - | Fecha de expiración | | `revoked_at` | TIMESTAMPTZ | NULL | - | Cuando fue revocado | | `revoked_reason` | VARCHAR(50) | NULL | - | Razón de revocación | | `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha de creación | #### Constraints ```sql -- Primary Key CONSTRAINT pk_refresh_tokens PRIMARY KEY (id), -- Unique CONSTRAINT uk_refresh_tokens_jti UNIQUE (jti), -- Foreign Keys CONSTRAINT fk_refresh_tokens_user FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE, CONSTRAINT fk_refresh_tokens_tenant FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, CONSTRAINT fk_refresh_tokens_replaced_by FOREIGN KEY (replaced_by) REFERENCES core_auth.refresh_tokens(id), -- Check CONSTRAINT chk_refresh_tokens_revoked_reason CHECK (revoked_reason IN ('user_logout', 'logout_all', 'token_rotation', 'security_breach', 'admin_action', 'password_change')) ``` #### Indices ```sql CREATE INDEX idx_refresh_tokens_user_id ON core_auth.refresh_tokens(user_id); CREATE INDEX idx_refresh_tokens_tenant_id ON core_auth.refresh_tokens(tenant_id); CREATE INDEX idx_refresh_tokens_family_id ON core_auth.refresh_tokens(family_id); CREATE INDEX idx_refresh_tokens_expires_at ON core_auth.refresh_tokens(expires_at) WHERE revoked_at IS NULL; CREATE INDEX idx_refresh_tokens_active ON core_auth.refresh_tokens(user_id, tenant_id) WHERE revoked_at IS NULL AND is_used = false; ``` --- ### 2. revoked_tokens Blacklist de access tokens revocados (para invalidación inmediata). #### Estructura | Columna | Tipo | Nullable | Default | Descripcion | |---------|------|----------|---------|-------------| | `id` | UUID | NOT NULL | gen_random_uuid() | PK | | `jti` | VARCHAR(64) | NOT NULL | - | JWT ID del token revocado | | `user_id` | UUID | NOT NULL | - | Usuario dueño del token | | `tenant_id` | UUID | NOT NULL | - | Tenant del token | | `token_type` | VARCHAR(20) | NOT NULL | 'access' | Tipo: access, refresh | | `original_exp` | TIMESTAMPTZ | NOT NULL | - | Expiración original del token | | `revocation_reason` | VARCHAR(50) | NOT NULL | - | Razón de revocación | | `revoked_at` | TIMESTAMPTZ | NOT NULL | NOW() | Cuando se revocó | | `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha de creación | #### Constraints ```sql CONSTRAINT pk_revoked_tokens PRIMARY KEY (id), CONSTRAINT uk_revoked_tokens_jti UNIQUE (jti), CONSTRAINT fk_revoked_tokens_user FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE, CONSTRAINT fk_revoked_tokens_tenant FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, CONSTRAINT chk_revoked_tokens_type CHECK (token_type IN ('access', 'refresh')) ``` #### Indices ```sql CREATE INDEX idx_revoked_tokens_jti ON core_auth.revoked_tokens(jti); CREATE INDEX idx_revoked_tokens_exp ON core_auth.revoked_tokens(original_exp); ``` > **Nota:** Esta tabla es un fallback. La blacklist principal debe estar en Redis para mejor performance. --- ### 3. session_history Historial de eventos de sesión para auditoría. #### Estructura | Columna | Tipo | Nullable | Default | Descripcion | |---------|------|----------|---------|-------------| | `id` | UUID | NOT NULL | gen_random_uuid() | PK | | `user_id` | UUID | NOT NULL | - | Usuario | | `tenant_id` | UUID | NOT NULL | - | Tenant | | `action` | VARCHAR(30) | NOT NULL | - | Tipo de evento | | `device_info` | VARCHAR(500) | NULL | - | User-Agent | | `ip_address` | INET | NULL | - | IP del cliente | | `metadata` | JSONB | NULL | '{}' | Datos adicionales | | `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Timestamp del evento | #### Constraints ```sql CONSTRAINT pk_session_history PRIMARY KEY (id), CONSTRAINT fk_session_history_user FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE, CONSTRAINT fk_session_history_tenant FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, CONSTRAINT chk_session_history_action CHECK (action IN ('login', 'logout', 'logout_all', 'refresh', 'password_change', 'password_reset', 'account_locked', 'account_unlocked', 'mfa_enabled', 'mfa_disabled')) ``` #### Indices ```sql CREATE INDEX idx_session_history_user_id ON core_auth.session_history(user_id); CREATE INDEX idx_session_history_tenant_id ON core_auth.session_history(tenant_id); CREATE INDEX idx_session_history_created_at ON core_auth.session_history(created_at DESC); CREATE INDEX idx_session_history_action ON core_auth.session_history(action); ``` --- ### 4. login_attempts Registro de intentos de login (exitosos y fallidos) para seguridad. #### Estructura | Columna | Tipo | Nullable | Default | Descripcion | |---------|------|----------|---------|-------------| | `id` | UUID | NOT NULL | gen_random_uuid() | PK | | `email` | VARCHAR(255) | NOT NULL | - | Email usado en intento | | `tenant_id` | UUID | NULL | - | Tenant (si aplica) | | `user_id` | UUID | NULL | - | Usuario (si existe) | | `success` | BOOLEAN | NOT NULL | - | Si fue exitoso | | `failure_reason` | VARCHAR(50) | NULL | - | Razón de fallo | | `ip_address` | INET | NOT NULL | - | IP del cliente | | `user_agent` | VARCHAR(500) | NULL | - | User-Agent | | `attempted_at` | TIMESTAMPTZ | NOT NULL | NOW() | Timestamp del intento | #### Constraints ```sql CONSTRAINT pk_login_attempts PRIMARY KEY (id), CONSTRAINT fk_login_attempts_tenant FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE SET NULL, CONSTRAINT fk_login_attempts_user FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE SET NULL, CONSTRAINT chk_login_attempts_failure_reason CHECK (failure_reason IS NULL OR failure_reason IN ('invalid_credentials', 'account_locked', 'account_inactive', 'tenant_inactive', 'mfa_required', 'mfa_failed')) ``` #### Indices ```sql CREATE INDEX idx_login_attempts_email ON core_auth.login_attempts(email); CREATE INDEX idx_login_attempts_ip ON core_auth.login_attempts(ip_address); CREATE INDEX idx_login_attempts_attempted_at ON core_auth.login_attempts(attempted_at DESC); CREATE INDEX idx_login_attempts_email_ip_recent ON core_auth.login_attempts(email, ip_address, attempted_at DESC); ``` --- ### 5. password_reset_tokens Tokens para recuperación de contraseña. #### Estructura | Columna | Tipo | Nullable | Default | Descripcion | |---------|------|----------|---------|-------------| | `id` | UUID | NOT NULL | gen_random_uuid() | PK | | `user_id` | UUID | NOT NULL | - | Usuario | | `tenant_id` | UUID | NOT NULL | - | Tenant | | `token_hash` | VARCHAR(255) | NOT NULL | - | Hash del token | | `attempts` | INTEGER | NOT NULL | 0 | Intentos de uso | | `expires_at` | TIMESTAMPTZ | NOT NULL | - | Expiración (1 hora) | | `used_at` | TIMESTAMPTZ | NULL | - | Cuando se usó | | `invalidated_at` | TIMESTAMPTZ | NULL | - | Cuando se invalidó | | `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Fecha de creación | #### Constraints ```sql CONSTRAINT pk_password_reset_tokens PRIMARY KEY (id), CONSTRAINT fk_password_reset_tokens_user FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE, CONSTRAINT fk_password_reset_tokens_tenant FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE, CONSTRAINT chk_password_reset_tokens_attempts CHECK (attempts >= 0 AND attempts <= 3) ``` #### Indices ```sql CREATE INDEX idx_password_reset_tokens_user_id ON core_auth.password_reset_tokens(user_id); CREATE INDEX idx_password_reset_tokens_expires_at ON core_auth.password_reset_tokens(expires_at); CREATE INDEX idx_password_reset_tokens_active ON core_auth.password_reset_tokens(user_id) WHERE used_at IS NULL AND invalidated_at IS NULL; ``` --- ### 6. password_history Historial de contraseñas para evitar reutilización. #### Estructura | Columna | Tipo | Nullable | Default | Descripcion | |---------|------|----------|---------|-------------| | `id` | UUID | NOT NULL | gen_random_uuid() | PK | | `user_id` | UUID | NOT NULL | - | Usuario | | `tenant_id` | UUID | NOT NULL | - | Tenant | | `password_hash` | VARCHAR(255) | NOT NULL | - | Hash de la contraseña | | `created_at` | TIMESTAMPTZ | NOT NULL | NOW() | Cuando se estableció | #### Constraints ```sql CONSTRAINT pk_password_history PRIMARY KEY (id), CONSTRAINT fk_password_history_user FOREIGN KEY (user_id) REFERENCES core_users.users(id) ON DELETE CASCADE, CONSTRAINT fk_password_history_tenant FOREIGN KEY (tenant_id) REFERENCES core_tenants.tenants(id) ON DELETE CASCADE ``` #### Indices ```sql CREATE INDEX idx_password_history_user_id ON core_auth.password_history(user_id); CREATE INDEX idx_password_history_user_recent ON core_auth.password_history(user_id, created_at DESC); ``` --- ## Row Level Security (RLS) ### Politicas de Seguridad ```sql -- Habilitar RLS en todas las tablas ALTER TABLE core_auth.refresh_tokens ENABLE ROW LEVEL SECURITY; ALTER TABLE core_auth.revoked_tokens ENABLE ROW LEVEL SECURITY; ALTER TABLE core_auth.session_history ENABLE ROW LEVEL SECURITY; ALTER TABLE core_auth.login_attempts ENABLE ROW LEVEL SECURITY; ALTER TABLE core_auth.password_reset_tokens ENABLE ROW LEVEL SECURITY; ALTER TABLE core_auth.password_history ENABLE ROW LEVEL SECURITY; -- Politica: Solo ver tokens del propio tenant CREATE POLICY tenant_isolation_refresh_tokens ON core_auth.refresh_tokens FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid); CREATE POLICY tenant_isolation_revoked_tokens ON core_auth.revoked_tokens FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid); CREATE POLICY tenant_isolation_session_history ON core_auth.session_history FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid); CREATE POLICY tenant_isolation_login_attempts ON core_auth.login_attempts FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid OR tenant_id IS NULL); CREATE POLICY tenant_isolation_password_reset ON core_auth.password_reset_tokens FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid); CREATE POLICY tenant_isolation_password_history ON core_auth.password_history FOR ALL USING (tenant_id = current_setting('app.current_tenant_id')::uuid); ``` --- ## Triggers ### 1. Limpieza automática de tokens expirados ```sql -- Función para limpiar tokens expirados CREATE OR REPLACE FUNCTION core_auth.cleanup_expired_tokens() RETURNS TRIGGER AS $$ BEGIN -- Eliminar refresh tokens expirados hace más de 7 días DELETE FROM core_auth.refresh_tokens WHERE expires_at < NOW() - INTERVAL '7 days'; -- Eliminar revoked tokens cuyo original_exp pasó DELETE FROM core_auth.revoked_tokens WHERE original_exp < NOW(); -- Eliminar password_reset_tokens expirados hace más de 24 horas DELETE FROM core_auth.password_reset_tokens WHERE expires_at < NOW() - INTERVAL '24 hours'; RETURN NULL; END; $$ LANGUAGE plpgsql; -- Nota: Este trigger se ejecuta via pg_cron o job scheduler, no en cada INSERT ``` ### 2. Invalidar tokens anteriores al crear nuevo reset ```sql CREATE OR REPLACE FUNCTION core_auth.invalidate_previous_reset_tokens() RETURNS TRIGGER AS $$ BEGIN UPDATE core_auth.password_reset_tokens SET invalidated_at = NOW() WHERE user_id = NEW.user_id AND id != NEW.id AND used_at IS NULL AND invalidated_at IS NULL; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_invalidate_previous_reset_tokens AFTER INSERT ON core_auth.password_reset_tokens FOR EACH ROW EXECUTE FUNCTION core_auth.invalidate_previous_reset_tokens(); ``` ### 3. Mantener solo los últimos 5 passwords en historial ```sql CREATE OR REPLACE FUNCTION core_auth.limit_password_history() RETURNS TRIGGER AS $$ BEGIN -- Eliminar passwords antiguos, mantener solo los últimos 5 DELETE FROM core_auth.password_history WHERE user_id = NEW.user_id AND id NOT IN ( SELECT id FROM core_auth.password_history WHERE user_id = NEW.user_id ORDER BY created_at DESC LIMIT 5 ); RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trg_limit_password_history AFTER INSERT ON core_auth.password_history FOR EACH ROW EXECUTE FUNCTION core_auth.limit_password_history(); ``` --- ## Funciones de Utilidad ### 1. Verificar si token está en blacklist ```sql CREATE OR REPLACE FUNCTION core_auth.is_token_revoked(p_jti VARCHAR) RETURNS BOOLEAN AS $$ BEGIN RETURN EXISTS ( SELECT 1 FROM core_auth.revoked_tokens WHERE jti = p_jti AND original_exp > NOW() ); END; $$ LANGUAGE plpgsql STABLE; ``` ### 2. Contar intentos fallidos recientes ```sql CREATE OR REPLACE FUNCTION core_auth.count_recent_failed_attempts( p_email VARCHAR, p_minutes INTEGER DEFAULT 30 ) RETURNS INTEGER AS $$ BEGIN RETURN ( SELECT COUNT(*) FROM core_auth.login_attempts WHERE email = p_email AND success = false AND attempted_at > NOW() - (p_minutes || ' minutes')::INTERVAL ); END; $$ LANGUAGE plpgsql STABLE; ``` ### 3. Revocar todos los tokens de un usuario ```sql CREATE OR REPLACE FUNCTION core_auth.revoke_all_user_tokens( p_user_id UUID, p_reason VARCHAR DEFAULT 'admin_action' ) RETURNS INTEGER AS $$ DECLARE affected_count INTEGER; BEGIN UPDATE core_auth.refresh_tokens SET revoked_at = NOW(), revoked_reason = p_reason WHERE user_id = p_user_id AND revoked_at IS NULL; GET DIAGNOSTICS affected_count = ROW_COUNT; RETURN affected_count; END; $$ LANGUAGE plpgsql; ``` --- ## Scripts de Migración ### Crear Schema ```sql -- 001_create_schema_core_auth.sql CREATE SCHEMA IF NOT EXISTS core_auth; COMMENT ON SCHEMA core_auth IS 'Schema para autenticación, tokens y sesiones'; ``` ### Crear Tablas ```sql -- 002_create_tables_core_auth.sql -- refresh_tokens CREATE TABLE IF NOT EXISTS core_auth.refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, tenant_id UUID NOT NULL, jti VARCHAR(64) NOT NULL UNIQUE, token_hash VARCHAR(255) NOT NULL, family_id UUID NOT NULL, is_used BOOLEAN NOT NULL DEFAULT false, used_at TIMESTAMPTZ, replaced_by UUID, device_info VARCHAR(500), ip_address INET, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, revoked_reason VARCHAR(50), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT fk_refresh_tokens_replaced_by FOREIGN KEY (replaced_by) REFERENCES core_auth.refresh_tokens(id), CONSTRAINT chk_refresh_tokens_revoked_reason CHECK (revoked_reason IS NULL OR revoked_reason IN ('user_logout', 'logout_all', 'token_rotation', 'security_breach', 'admin_action', 'password_change')) ); -- revoked_tokens CREATE TABLE IF NOT EXISTS core_auth.revoked_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), jti VARCHAR(64) NOT NULL UNIQUE, user_id UUID NOT NULL, tenant_id UUID NOT NULL, token_type VARCHAR(20) NOT NULL DEFAULT 'access', original_exp TIMESTAMPTZ NOT NULL, revocation_reason VARCHAR(50) NOT NULL, revoked_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_revoked_tokens_type CHECK (token_type IN ('access', 'refresh')) ); -- session_history CREATE TABLE IF NOT EXISTS core_auth.session_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, tenant_id UUID NOT NULL, action VARCHAR(30) NOT NULL, device_info VARCHAR(500), ip_address INET, metadata JSONB DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_session_history_action CHECK (action IN ('login', 'logout', 'logout_all', 'refresh', 'password_change', 'password_reset', 'account_locked', 'account_unlocked', 'mfa_enabled', 'mfa_disabled')) ); -- login_attempts CREATE TABLE IF NOT EXISTS core_auth.login_attempts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) NOT NULL, tenant_id UUID, user_id UUID, success BOOLEAN NOT NULL, failure_reason VARCHAR(50), ip_address INET NOT NULL, user_agent VARCHAR(500), attempted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_login_attempts_failure_reason CHECK ( failure_reason IS NULL OR failure_reason IN ('invalid_credentials', 'account_locked', 'account_inactive', 'tenant_inactive', 'mfa_required', 'mfa_failed')) ); -- password_reset_tokens CREATE TABLE IF NOT EXISTS core_auth.password_reset_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, tenant_id UUID NOT NULL, token_hash VARCHAR(255) NOT NULL, attempts INTEGER NOT NULL DEFAULT 0, expires_at TIMESTAMPTZ NOT NULL, used_at TIMESTAMPTZ, invalidated_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT chk_password_reset_tokens_attempts CHECK (attempts >= 0 AND attempts <= 3) ); -- password_history CREATE TABLE IF NOT EXISTS core_auth.password_history ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL, tenant_id UUID NOT NULL, password_hash VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` ### Crear Indices ```sql -- 003_create_indexes_core_auth.sql -- refresh_tokens CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON core_auth.refresh_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_tenant_id ON core_auth.refresh_tokens(tenant_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_family_id ON core_auth.refresh_tokens(family_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON core_auth.refresh_tokens(expires_at) WHERE revoked_at IS NULL; CREATE INDEX IF NOT EXISTS idx_refresh_tokens_active ON core_auth.refresh_tokens(user_id, tenant_id) WHERE revoked_at IS NULL AND is_used = false; -- revoked_tokens CREATE INDEX IF NOT EXISTS idx_revoked_tokens_jti ON core_auth.revoked_tokens(jti); CREATE INDEX IF NOT EXISTS idx_revoked_tokens_exp ON core_auth.revoked_tokens(original_exp); -- session_history CREATE INDEX IF NOT EXISTS idx_session_history_user_id ON core_auth.session_history(user_id); CREATE INDEX IF NOT EXISTS idx_session_history_tenant_id ON core_auth.session_history(tenant_id); CREATE INDEX IF NOT EXISTS idx_session_history_created_at ON core_auth.session_history(created_at DESC); CREATE INDEX IF NOT EXISTS idx_session_history_action ON core_auth.session_history(action); -- login_attempts CREATE INDEX IF NOT EXISTS idx_login_attempts_email ON core_auth.login_attempts(email); CREATE INDEX IF NOT EXISTS idx_login_attempts_ip ON core_auth.login_attempts(ip_address); CREATE INDEX IF NOT EXISTS idx_login_attempts_attempted_at ON core_auth.login_attempts(attempted_at DESC); CREATE INDEX IF NOT EXISTS idx_login_attempts_email_ip_recent ON core_auth.login_attempts(email, ip_address, attempted_at DESC); -- password_reset_tokens CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_user_id ON core_auth.password_reset_tokens(user_id); CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_expires_at ON core_auth.password_reset_tokens(expires_at); CREATE INDEX IF NOT EXISTS idx_password_reset_tokens_active ON core_auth.password_reset_tokens(user_id) WHERE used_at IS NULL AND invalidated_at IS NULL; -- password_history CREATE INDEX IF NOT EXISTS idx_password_history_user_id ON core_auth.password_history(user_id); CREATE INDEX IF NOT EXISTS idx_password_history_user_recent ON core_auth.password_history(user_id, created_at DESC); ``` --- ## Consideraciones de Performance | Tabla | Volumen Esperado | Estrategia | |-------|------------------|------------| | refresh_tokens | Alto (miles/día) | Particionamiento por fecha, cleanup job | | revoked_tokens | Medio | TTL automático, Redis como primario | | session_history | Alto | Particionamiento por mes, archivado | | login_attempts | Alto | Retención 90 días, particionamiento | | password_reset_tokens | Bajo | Cleanup diario | | password_history | Bajo | Límite de 5 por usuario | --- ## Notas de Seguridad 1. **Nunca almacenar tokens planos** - Solo hashes 2. **RLS obligatorio** - Aislamiento por tenant 3. **Limpieza programada** - Eliminar datos expirados 4. **Auditoría completa** - session_history para compliance 5. **Indices eficientes** - Para consultas de validación rápidas --- ## Historial de Cambios | Version | Fecha | Autor | Cambios | |---------|-------|-------|---------| | 1.0 | 2025-12-05 | System | Creación inicial | --- ## Aprobaciones | Rol | Nombre | Fecha | Firma | |-----|--------|-------|-------| | DBA | - | - | [ ] | | Tech Lead | - | - | [ ] | | Security | - | - | [ ] |