24 KiB
24 KiB
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
usersreside encore_users, no en este schema.
Diagrama Entidad-Relacion
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
-- 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
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
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
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
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
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
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
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
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
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
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
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
-- 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
-- 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
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
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
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
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
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
-- 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
-- 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
-- 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
- Nunca almacenar tokens planos - Solo hashes
- RLS obligatorio - Aislamiento por tenant
- Limpieza programada - Eliminar datos expirados
- Auditoría completa - session_history para compliance
- 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 | - | - | [ ] |