-- ============================================================= -- ARCHIVO: 06-auth-extended.sql -- DESCRIPCION: Extensiones de autenticacion SaaS (JWT, OAuth, MFA) -- VERSION: 1.0.0 -- PROYECTO: ERP-Core V2 -- FECHA: 2026-01-10 -- EPIC: SAAS-CORE-AUTH (EPIC-SAAS-001) -- HISTORIAS: US-001, US-002, US-003 -- ============================================================= -- ===================== -- MODIFICACIONES A TABLAS EXISTENTES -- ===================== -- Agregar columnas OAuth a auth.users (US-002) ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS oauth_provider VARCHAR(50); ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS oauth_provider_id VARCHAR(255); ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS avatar_url TEXT; -- Agregar columnas MFA a auth.users (US-003) ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS mfa_enabled BOOLEAN DEFAULT FALSE; ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS mfa_secret_encrypted TEXT; ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS mfa_backup_codes TEXT[]; -- Agregar columna superadmin (EPIC-SAAS-006) ALTER TABLE auth.users ADD COLUMN IF NOT EXISTS is_superadmin BOOLEAN DEFAULT FALSE; -- Indices para nuevas columnas CREATE INDEX IF NOT EXISTS idx_users_oauth_provider ON auth.users(oauth_provider, oauth_provider_id) WHERE oauth_provider IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_users_mfa_enabled ON auth.users(mfa_enabled) WHERE mfa_enabled = TRUE; -- ===================== -- TABLA: auth.sessions -- Sesiones de usuario con refresh tokens (US-001) -- ===================== CREATE TABLE IF NOT EXISTS auth.sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Token info refresh_token_hash VARCHAR(255) NOT NULL, jti VARCHAR(255) UNIQUE NOT NULL, -- JWT ID para blacklist -- Device info device_info JSONB DEFAULT '{}', device_fingerprint VARCHAR(255), user_agent TEXT, ip_address INET, -- Geo info country_code VARCHAR(2), city VARCHAR(100), -- Timestamps last_activity_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ, revoked_reason VARCHAR(100), created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- Indices para sessions CREATE INDEX IF NOT EXISTS idx_sessions_user ON auth.sessions(user_id); CREATE INDEX IF NOT EXISTS idx_sessions_tenant ON auth.sessions(tenant_id); CREATE INDEX IF NOT EXISTS idx_sessions_jti ON auth.sessions(jti); CREATE INDEX IF NOT EXISTS idx_sessions_expires ON auth.sessions(expires_at) WHERE revoked_at IS NULL; CREATE INDEX IF NOT EXISTS idx_sessions_active ON auth.sessions(user_id, revoked_at) WHERE revoked_at IS NULL; -- ===================== -- TABLA: auth.token_blacklist -- Tokens revocados/invalidados (US-001) -- ===================== CREATE TABLE IF NOT EXISTS auth.token_blacklist ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), jti VARCHAR(255) UNIQUE NOT NULL, user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, token_type VARCHAR(20) NOT NULL CHECK (token_type IN ('access', 'refresh')), -- Metadata reason VARCHAR(100), revoked_by UUID REFERENCES auth.users(id), -- Timestamps expires_at TIMESTAMPTZ NOT NULL, revoked_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- Indice para limpieza de tokens expirados CREATE INDEX IF NOT EXISTS idx_token_blacklist_expires ON auth.token_blacklist(expires_at); CREATE INDEX IF NOT EXISTS idx_token_blacklist_jti ON auth.token_blacklist(jti); -- ===================== -- TABLA: auth.oauth_providers -- Proveedores OAuth vinculados a usuarios (US-002) -- ===================== CREATE TABLE IF NOT EXISTS auth.oauth_providers ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, tenant_id UUID NOT NULL REFERENCES auth.tenants(id) ON DELETE CASCADE, -- Provider info provider VARCHAR(50) NOT NULL, -- google, github, microsoft, apple provider_user_id VARCHAR(255) NOT NULL, provider_email VARCHAR(255), -- Tokens (encrypted) access_token_encrypted TEXT, refresh_token_encrypted TEXT, token_expires_at TIMESTAMPTZ, -- Profile data from provider profile_data JSONB DEFAULT '{}', -- Timestamps linked_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, last_used_at TIMESTAMPTZ, unlinked_at TIMESTAMPTZ, UNIQUE(provider, provider_user_id) ); -- Indices para oauth_providers CREATE INDEX IF NOT EXISTS idx_oauth_providers_user ON auth.oauth_providers(user_id); CREATE INDEX IF NOT EXISTS idx_oauth_providers_provider ON auth.oauth_providers(provider, provider_user_id); CREATE INDEX IF NOT EXISTS idx_oauth_providers_email ON auth.oauth_providers(provider_email); -- ===================== -- TABLA: auth.mfa_devices -- Dispositivos MFA registrados (US-003) -- ===================== CREATE TABLE IF NOT EXISTS auth.mfa_devices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, -- Device info device_type VARCHAR(50) NOT NULL, -- totp, sms, email, hardware_key device_name VARCHAR(255), -- TOTP specific secret_encrypted TEXT, -- Status is_primary BOOLEAN DEFAULT FALSE, is_verified BOOLEAN DEFAULT FALSE, verified_at TIMESTAMPTZ, -- Usage tracking last_used_at TIMESTAMPTZ, use_count INTEGER DEFAULT 0, -- Timestamps created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, disabled_at TIMESTAMPTZ ); -- Indices para mfa_devices CREATE INDEX IF NOT EXISTS idx_mfa_devices_user ON auth.mfa_devices(user_id); CREATE INDEX IF NOT EXISTS idx_mfa_devices_primary ON auth.mfa_devices(user_id, is_primary) WHERE is_primary = TRUE; -- ===================== -- TABLA: auth.mfa_backup_codes -- Codigos de respaldo MFA (US-003) -- ===================== CREATE TABLE IF NOT EXISTS auth.mfa_backup_codes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, -- Code (hashed) code_hash VARCHAR(255) NOT NULL, -- Status used_at TIMESTAMPTZ, -- Timestamps created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP, expires_at TIMESTAMPTZ ); -- Indices para mfa_backup_codes CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_user ON auth.mfa_backup_codes(user_id); CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_unused ON auth.mfa_backup_codes(user_id, used_at) WHERE used_at IS NULL; -- ===================== -- TABLA: auth.login_attempts -- Intentos de login para rate limiting y seguridad -- ===================== CREATE TABLE IF NOT EXISTS auth.login_attempts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Identificacion email VARCHAR(255), ip_address INET NOT NULL, user_agent TEXT, -- Resultado success BOOLEAN NOT NULL, failure_reason VARCHAR(100), -- MFA mfa_required BOOLEAN DEFAULT FALSE, mfa_passed BOOLEAN, -- Metadata tenant_id UUID REFERENCES auth.tenants(id), user_id UUID REFERENCES auth.users(id), created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP ); -- Indices para login_attempts CREATE INDEX IF NOT EXISTS idx_login_attempts_email ON auth.login_attempts(email, created_at DESC); CREATE INDEX IF NOT EXISTS idx_login_attempts_ip ON auth.login_attempts(ip_address, created_at DESC); CREATE INDEX IF NOT EXISTS idx_login_attempts_cleanup ON auth.login_attempts(created_at); -- ===================== -- RLS POLICIES -- ===================== ALTER TABLE auth.sessions ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_sessions ON auth.sessions USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE auth.oauth_providers ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation_oauth ON auth.oauth_providers USING (tenant_id = current_setting('app.current_tenant_id', true)::uuid); ALTER TABLE auth.mfa_devices ENABLE ROW LEVEL SECURITY; CREATE POLICY user_isolation_mfa_devices ON auth.mfa_devices USING (user_id = current_setting('app.current_user_id', true)::uuid); ALTER TABLE auth.mfa_backup_codes ENABLE ROW LEVEL SECURITY; CREATE POLICY user_isolation_mfa_codes ON auth.mfa_backup_codes USING (user_id = current_setting('app.current_user_id', true)::uuid); -- ===================== -- FUNCIONES -- ===================== -- Funcion para limpiar sesiones expiradas CREATE OR REPLACE FUNCTION auth.cleanup_expired_sessions() RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN DELETE FROM auth.sessions WHERE expires_at < CURRENT_TIMESTAMP OR revoked_at IS NOT NULL; GET DIAGNOSTICS deleted_count = ROW_COUNT; RETURN deleted_count; END; $$ LANGUAGE plpgsql; -- Funcion para limpiar tokens expirados del blacklist CREATE OR REPLACE FUNCTION auth.cleanup_expired_tokens() RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN DELETE FROM auth.token_blacklist WHERE expires_at < CURRENT_TIMESTAMP; GET DIAGNOSTICS deleted_count = ROW_COUNT; RETURN deleted_count; END; $$ LANGUAGE plpgsql; -- Funcion para limpiar intentos de login antiguos (mas de 30 dias) CREATE OR REPLACE FUNCTION auth.cleanup_old_login_attempts() RETURNS INTEGER AS $$ DECLARE deleted_count INTEGER; BEGIN DELETE FROM auth.login_attempts WHERE created_at < CURRENT_TIMESTAMP - INTERVAL '30 days'; GET DIAGNOSTICS deleted_count = ROW_COUNT; RETURN deleted_count; END; $$ LANGUAGE plpgsql; -- Funcion para verificar rate limit de login CREATE OR REPLACE FUNCTION auth.check_login_rate_limit( p_email VARCHAR(255), p_ip_address INET, p_max_attempts INTEGER DEFAULT 5, p_window_minutes INTEGER DEFAULT 15 ) RETURNS BOOLEAN AS $$ DECLARE attempt_count INTEGER; BEGIN SELECT COUNT(*) INTO attempt_count FROM auth.login_attempts WHERE (email = p_email OR ip_address = p_ip_address) AND success = FALSE AND created_at > CURRENT_TIMESTAMP - (p_window_minutes || ' minutes')::INTERVAL; RETURN attempt_count < p_max_attempts; END; $$ LANGUAGE plpgsql; -- Funcion para revocar todas las sesiones de un usuario CREATE OR REPLACE FUNCTION auth.revoke_all_user_sessions( p_user_id UUID, p_reason VARCHAR(100) DEFAULT 'manual_revocation' ) RETURNS INTEGER AS $$ DECLARE revoked_count INTEGER; BEGIN UPDATE auth.sessions SET revoked_at = CURRENT_TIMESTAMP, revoked_reason = p_reason WHERE user_id = p_user_id AND revoked_at IS NULL; GET DIAGNOSTICS revoked_count = ROW_COUNT; RETURN revoked_count; END; $$ LANGUAGE plpgsql; -- ===================== -- COMENTARIOS -- ===================== COMMENT ON TABLE auth.sessions IS 'Sesiones de usuario con refresh tokens para JWT'; COMMENT ON TABLE auth.token_blacklist IS 'Tokens JWT revocados antes de su expiracion'; COMMENT ON TABLE auth.oauth_providers IS 'Proveedores OAuth vinculados a cuentas de usuario'; COMMENT ON TABLE auth.mfa_devices IS 'Dispositivos MFA registrados por usuario'; COMMENT ON TABLE auth.mfa_backup_codes IS 'Codigos de respaldo para MFA'; COMMENT ON TABLE auth.login_attempts IS 'Registro de intentos de login para seguridad'; COMMENT ON FUNCTION auth.cleanup_expired_sessions IS 'Limpia sesiones expiradas o revocadas'; COMMENT ON FUNCTION auth.cleanup_expired_tokens IS 'Limpia tokens del blacklist que ya expiraron'; COMMENT ON FUNCTION auth.check_login_rate_limit IS 'Verifica si un email/IP ha excedido intentos de login'; COMMENT ON FUNCTION auth.revoke_all_user_sessions IS 'Revoca todas las sesiones activas de un usuario';