/** * IP Restriction Middleware * Checks if the client IP is allowed access based on tenant configuration */ const { Tenant } = require('../models'); const MultiTenantAuth = require('./multi-tenant-auth'); class IPRestrictionMiddleware { constructor() { this.multiAuth = new MultiTenantAuth(); this.models = null; // For dependency injection in tests } /** * Set models for dependency injection (used in tests) * @param {Object} models - Models object */ setModels(models) { this.models = models; this.multiAuth.setModels && this.multiAuth.setModels(models); } /** * Check if an IP address matches any pattern in the whitelist * @param {string} clientIP - The client IP address * @param {Array} whitelist - Array of IP addresses and CIDR blocks * @returns {boolean} - True if IP is allowed */ isIPAllowed(clientIP, whitelist) { if (!whitelist || !Array.isArray(whitelist) || whitelist.length === 0) { return false; // Block access if no IPs are whitelisted } // Normalize IPv6-mapped IPv4 addresses const normalizedIP = clientIP.replace(/^::ffff:/, ''); for (const allowedIP of whitelist) { if (this.matchesPattern(normalizedIP, allowedIP.trim())) { return true; } } return false; } /** * Check if an IP matches a pattern (IP address or CIDR block) * @param {string} ip - The IP to check * @param {string} pattern - IP address or CIDR block * @returns {boolean} - True if IP matches pattern */ matchesPattern(ip, pattern) { // Exact IP match if (ip === pattern) { return true; } // CIDR block matching if (pattern.includes('/')) { return this.isIPInCIDR(ip, pattern); } // Wildcard matching (e.g., 192.168.1.*) if (pattern.includes('*')) { const regex = new RegExp('^' + pattern.replace(/\./g, '\\.').replace(/\*/g, '\\d+') + '$'); return regex.test(ip); } return false; } /** * Check if an IP is within a CIDR block * @param {string} ip - The IP to check * @param {string} cidr - CIDR block (e.g., "192.168.1.0/24") * @returns {boolean} - True if IP is in CIDR block */ isIPInCIDR(ip, cidr) { try { const [subnet, prefixLength] = cidr.split('/'); const prefix = parseInt(prefixLength, 10); if (prefix < 0 || prefix > 32) { return false; } const ipNum = this.ipToNumber(ip); const subnetNum = this.ipToNumber(subnet); const mask = (-1 << (32 - prefix)) >>> 0; return (ipNum & mask) === (subnetNum & mask); } catch (error) { console.error('Error checking CIDR:', error); return false; } } /** * Convert IP address to number * @param {string} ip - IP address * @returns {number} - IP as number */ ipToNumber(ip) { return ip.split('.').reduce((acc, octet) => (acc << 8) + parseInt(octet, 10), 0) >>> 0; } /** * Get client IP from request, considering proxy headers * @param {Object} req - Express request object * @returns {string} - Client IP address */ getClientIP(req) { // Check various headers for real IP (in order of preference) const possibleHeaders = [ 'x-forwarded-for', 'x-real-ip', 'x-client-ip', 'cf-connecting-ip', // Cloudflare 'x-cluster-client-ip', 'x-forwarded', 'forwarded-for', 'forwarded' ]; for (const header of possibleHeaders) { const ip = req.headers[header]; if (ip) { // x-forwarded-for can contain multiple IPs, get the first one const firstIP = ip.split(',')[0].trim(); if (this.isValidIP(firstIP)) { return firstIP; } } } // Fallback to connection IP return req.connection?.remoteAddress || req.socket?.remoteAddress || req.ip || 'unknown'; } /** * Basic IP validation * @param {string} ip - IP address to validate * @returns {boolean} - True if valid IP */ isValidIP(ip) { const ipv4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; const ipv6Regex = /^(?:[0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$/; return ipv4Regex.test(ip) || ipv6Regex.test(ip); } /** * Express middleware to check IP restrictions * @param {Object} req - Express request object * @param {Object} res - Express response object * @param {Function} next - Next middleware function */ async checkIPRestriction(req, res, next) { try { // Ensure req.path exists const path = req.path || req.url || ''; // Skip IP checking for health checks and internal requests if (path === '/health' || path === '/api/health') { return next(); } // Skip IP restrictions for management routes - they have their own access controls if (path.startsWith('/api/management/')) { console.log('🔍 IP Restriction - Skipping for management route:', path); return next(); } // Skip IP restrictions for auth config - users need to see login form and get proper error if (path === '/api/auth/config') { console.log('🔍 IP Restriction - Skipping for auth config route'); return next(); } console.log('🔍 IP Restriction Check - Path:', req.path, 'Method:', req.method); // Determine tenant (check req.tenant first for test contexts) let tenantId = req.tenant; if (!tenantId) { tenantId = await this.multiAuth.determineTenant(req); } console.log('🔍 IP Restriction - Determined tenant:', tenantId); if (!tenantId) { console.log('🔍 IP Restriction - No tenant found, skipping IP check'); // No tenant found, continue without IP checking return next(); } // Get tenant configuration const TenantModel = this.models ? this.models.Tenant : Tenant; const tenant = await TenantModel.findOne({ where: { slug: tenantId }, attributes: ['id', 'slug', 'ip_restriction_enabled', 'ip_whitelist', 'ip_restriction_message', 'updated_at'] }); if (!tenant) { console.log('🔍 IP Restriction - Tenant not found in database:', tenantId); return next(); } console.log('🔍 IP Restriction - Tenant config (fresh from DB):', { id: tenant.id, slug: tenant.slug, ip_restriction_enabled: tenant.ip_restriction_enabled, ip_whitelist: tenant.ip_whitelist, updated_at: tenant.updated_at }); // Check if IP restrictions are enabled if (!tenant.ip_restriction_enabled) { console.log('🔍 IP Restriction - Restrictions disabled for tenant'); return next(); } // Get client IP const clientIP = this.getClientIP(req); console.log('🔍 IP Restriction - Client IP:', clientIP); console.log('🔍 IP Restriction - Request headers:', { 'x-forwarded-for': req.headers['x-forwarded-for'], 'x-real-ip': req.headers['x-real-ip'], 'remote-address': req.connection?.remoteAddress }); // Parse allowed IPs (convert string to array) let allowedIPs = []; if (tenant.ip_whitelist) { if (Array.isArray(tenant.ip_whitelist)) { allowedIPs = tenant.ip_whitelist; } else if (typeof tenant.ip_whitelist === 'string') { allowedIPs = tenant.ip_whitelist.split(',').map(ip => ip.trim()).filter(ip => ip); } } // Check if IP is allowed const isAllowed = this.isIPAllowed(clientIP, allowedIPs); console.log('🔍 IP Restriction - Is IP allowed:', isAllowed, 'Allowed IPs:', allowedIPs); if (!isAllowed) { console.log(`🚫 IP Access Denied: ${clientIP} attempted to access tenant "${tenantId}"`); // Log the access attempt for security auditing console.log(`[SECURITY AUDIT] ${new Date().toISOString()} - IP ${clientIP} denied access to tenant ${tenantId} - User-Agent: ${req.headers['user-agent']}`); return res.status(403).json({ success: false, message: tenant.ip_restriction_message || 'Access denied. Your IP address is not authorized to access this service.', code: 'IP_RESTRICTED', timestamp: new Date().toISOString() }); } // IP is allowed, continue console.log(`✅ IP Access Allowed: ${clientIP} accessing tenant "${tenantId}"`); next(); } catch (error) { console.error('Error in IP restriction middleware:', error); // In case of error, allow access but log the issue next(); } } } module.exports = IPRestrictionMiddleware;