erp-core/docs/04-modelado/database-design/DDL-SPEC-core_auth.md

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 users reside en core_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

  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 - - [ ]