-- MFA (Multi-Factor Authentication) Tables -- Created in Wave 112 to support TOTP-based MFA functionality -- Addresses CVSS 9.1 vulnerability by enforcing MFA for all users -- MFA configuration per user CREATE TABLE IF NOT EXISTS mfa_config ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID UNIQUE NOT NULL REFERENCES users(id) ON DELETE CASCADE, totp_secret_encrypted BYTEA NOT NULL, totp_algorithm VARCHAR(10) DEFAULT 'SHA1' NOT NULL, totp_digits INTEGER DEFAULT 6 NOT NULL, totp_period INTEGER DEFAULT 30 NOT NULL, is_enabled BOOLEAN DEFAULT FALSE NOT NULL, is_verified BOOLEAN DEFAULT FALSE NOT NULL, enrolled_at TIMESTAMP WITH TIME ZONE, verified_at TIMESTAMP WITH TIME ZONE, last_used_at TIMESTAMP WITH TIME ZONE, backup_codes_remaining INTEGER DEFAULT 0 NOT NULL, failed_verification_attempts INTEGER DEFAULT 0 NOT NULL, last_failed_attempt_at TIMESTAMP WITH TIME ZONE, locked_until TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL, updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); -- MFA backup codes for account recovery CREATE TABLE IF NOT EXISTS mfa_backup_codes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, code_hash VARCHAR(64) NOT NULL, -- SHA-256 hash code_hint VARCHAR(10) NOT NULL, -- First 4 characters for user reference is_used BOOLEAN DEFAULT FALSE NOT NULL, used_at TIMESTAMP WITH TIME ZONE, used_from_ip INET, -- IP address where code was used expires_at TIMESTAMP WITH TIME ZONE NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_user_id ON mfa_backup_codes(user_id); CREATE INDEX IF NOT EXISTS idx_mfa_backup_codes_active ON mfa_backup_codes(user_id, is_used) WHERE is_used = FALSE; -- MFA enrollment sessions (temporary, 15 min TTL) CREATE TABLE IF NOT EXISTS mfa_enrollment_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, temp_totp_secret_encrypted BYTEA NOT NULL, qr_code_data TEXT NOT NULL, is_active BOOLEAN DEFAULT TRUE NOT NULL, verification_attempts INTEGER DEFAULT 0 NOT NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, completed_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mfa_enrollment_sessions_user_id ON mfa_enrollment_sessions(user_id); CREATE INDEX IF NOT EXISTS idx_mfa_enrollment_sessions_active ON mfa_enrollment_sessions(id, is_active, expires_at) WHERE is_active = TRUE; -- MFA verification audit log CREATE TABLE IF NOT EXISTS mfa_verification_log ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, method VARCHAR(20) NOT NULL, -- 'totp', 'backup_code', 'trusted_device' success BOOLEAN NOT NULL, ip_address INET, user_agent TEXT, device_id UUID, error_code VARCHAR(50), created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() NOT NULL ); CREATE INDEX IF NOT EXISTS idx_mfa_verification_log_user_id ON mfa_verification_log(user_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_mfa_verification_log_failed ON mfa_verification_log(user_id, success, created_at DESC) WHERE success = FALSE; -- Function to check if MFA is required for a user CREATE OR REPLACE FUNCTION is_mfa_required(p_user_id UUID) RETURNS BOOLEAN AS $$ BEGIN -- In production, this would check user role, department, access level, etc. -- For now, MFA is required for all users RETURN TRUE; END; $$ LANGUAGE plpgsql STABLE; -- Function to record MFA attempt CREATE OR REPLACE FUNCTION record_mfa_attempt( p_user_id UUID, p_method VARCHAR(20), p_success BOOLEAN, p_ip_address VARCHAR(45), p_user_agent TEXT, p_device_id UUID, p_error_code VARCHAR(50) ) RETURNS UUID AS $$ DECLARE v_log_id UUID; BEGIN -- Insert verification log INSERT INTO mfa_verification_log ( user_id, method, success, ip_address, user_agent, device_id, error_code ) VALUES ( p_user_id, p_method, p_success, p_ip_address::INET, p_user_agent, p_device_id, p_error_code ) RETURNING id INTO v_log_id; -- Update MFA config IF p_success THEN UPDATE mfa_config SET last_used_at = NOW(), failed_verification_attempts = 0, last_failed_attempt_at = NULL, locked_until = NULL WHERE user_id = p_user_id; ELSE UPDATE mfa_config SET failed_verification_attempts = failed_verification_attempts + 1, last_failed_attempt_at = NOW(), locked_until = CASE WHEN failed_verification_attempts + 1 >= 5 THEN NOW() + INTERVAL '30 minutes' ELSE NULL END WHERE user_id = p_user_id; END IF; RETURN v_log_id; END; $$ LANGUAGE plpgsql;