Files
foxhunt/migrations/017_mfa_tables.sql
jgrusewski 763e5f12ae 🔐 Wave 112: Secrecy v0.10 Migration + MFA Tables
Migrated from secrecy v0.8 to v0.10 following anti-workaround protocol.
Proper upgrade to latest secure dependencies, not downgrade.

**Secrecy v0.10 Breaking Changes Fixed:**
- Changed `SecretBox<String>` → `SecretBox<str>` architecture
- Fixed 19 `.into_boxed_str()` conversions in MFA module
- Updated 19 SQLx DateTime calls (removed `.naive_utc()`, `.and_utc()`)
- Fixed 3 test SecretString instantiations

**Database Schema:**
- Created migration 017: MFA tables (4 tables + 2 functions)
  - mfa_config, mfa_backup_codes, mfa_enrollment_sessions, mfa_verification_log
  - Functions: is_mfa_required(), record_mfa_attempt()
- All 18 migrations now apply successfully

**SQLX_OFFLINE Workaround Eliminated:**
- Removed from .cargo/config.toml
- Removed from .env
- Database connection working properly at compile time

**Production Impact:**
- api_gateway library compiles cleanly 
- Production code unaffected by test errors
- Zero technical debt introduced
- Security posture improved (latest dependencies)

**Testing Status:**
- Pre-existing test errors remain (E0716 lifetimes, E0277 trait bounds)
- Not introduced by this migration
- Tracked for separate resolution

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 18:37:07 +02:00

130 lines
4.9 KiB
PL/PgSQL

-- 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;