/** * Multi-Tenant Authentication Middleware * Routes authentication requests to appropriate providers */ console.log('MULTI-TENANT-AUTH MODULE LOADED'); const jwt = require('jsonwebtoken'); const { User, Tenant } = require('../models'); const { AuthConfig, AuthProviders } = require('../config/auth-providers'); const SAMLAuth = require('./saml-auth'); const OAuthAuth = require('./oauth-auth'); const LDAPAuth = require('./ldap-auth'); class MultiTenantAuth { constructor() { this.authConfig = new AuthConfig(); this.providers = new Map(); this.models = null; // For dependency injection in tests // Initialize authentication providers this.initializeProviders(); } /** * Set models for dependency injection (used in tests) * @param {Object} models - Models object */ setModels(models) { this.models = models; } /** * Check if a string is an IP address */ isIPAddress(str) { 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(str) || ipv6Regex.test(str); } /** * Initialize all authentication providers */ initializeProviders() { this.providers.set(AuthProviders.SAML, new SAMLAuth()); this.providers.set(AuthProviders.OAUTH, new OAuthAuth()); this.providers.set(AuthProviders.LDAP, new LDAPAuth()); } /** * Determine tenant from request * Can be from subdomain, header, or JWT */ async determineTenant(req) { // Method 1: From authenticated user (highest priority) if (req.user && req.user.tenantId) { return req.user.tenantId; } // Method 2: From JWT token (for existing sessions) const token = req.headers.authorization?.split(' ')[1]; if (token) { try { const decoded = jwt.verify(token, process.env.JWT_SECRET || 'test-secret'); if (decoded.tenantId) { return decoded.tenantId; } } catch (error) { // Token invalid, continue with other methods } } // Method 3: Custom header const tenantHeader = req.headers['x-tenant-id']; if (tenantHeader) { return tenantHeader; } // Method 4: x-forwarded-host header (for proxied requests) const forwardedHost = req.headers['x-forwarded-host']; if (forwardedHost) { const subdomain = forwardedHost.split('.')[0]; if (subdomain && subdomain !== 'www' && subdomain !== 'api' && !subdomain.includes(':')) { return subdomain; } } // Method 5: Subdomain (tenant.yourapp.com) const hostname = req.hostname || req.headers.host || ''; // Remove port number if present const hostWithoutPort = hostname.split(':')[0]; // Skip if localhost or IP address if (hostname && !hostname.startsWith('localhost') && !this.isIPAddress(hostWithoutPort)) { const hostParts = hostWithoutPort.split('.'); // Only treat as subdomain if there are at least 2 parts (subdomain.domain.com) // and the first part is not a common root domain if (hostParts.length >= 3) { const subdomain = hostParts[0]; if (subdomain && subdomain !== 'www' && subdomain !== 'api') { return subdomain; } } } // Method 6: URL path (/tenant2/api/...) const urlPath = req.path || req.url || ''; const pathSegments = urlPath.split('/').filter(segment => segment); if (pathSegments.length > 0 && pathSegments[0] !== 'api') { return pathSegments[0]; } // Method 7: Query parameter (for redirects) if (req.query && req.query.tenant) { return req.query.tenant; } // Return null for localhost without tenant info if (hostname && hostname.startsWith('localhost')) { return null; } // Default to null return null; } /** * Get authentication configuration for tenant */ async getTenantAuthConfig(tenantId) { const TenantModel = this.models ? this.models.Tenant : Tenant; const tenant = await TenantModel.findOne({ where: { slug: tenantId } }); if (!tenant) { // Return default local auth for unknown tenants return { type: AuthProviders.LOCAL, enabled: true, config: {}, userMapping: this.authConfig.getDefaultUserMapping(), roleMapping: this.authConfig.getDefaultRoleMapping() }; } return this.authConfig.getProvider(tenantId); } /** * Authenticate user based on tenant configuration */ async authenticate(req, res, next) { try { const tenantId = await this.determineTenant(req); // Check if tenant could be determined if (!tenantId) { return res.status(400).json({ success: false, message: 'Unable to determine tenant' }); } // Check if tenant exists in database const TenantModel = this.models ? this.models.Tenant : Tenant; const tenant = await TenantModel.findOne({ where: { slug: tenantId } }); if (!tenant) { return res.status(404).json({ success: false, message: 'Tenant not found' }); } // Check if tenant is active if (!tenant.is_active) { return res.status(403).json({ success: false, message: 'Tenant is not active' }); } // Attach tenant info to request (tests expect req.tenant to be the slug) req.tenant = tenantId; // Call next middleware next(); } catch (error) { console.error('Multi-tenant auth error:', error); return res.status(500).json({ success: false, message: 'Authentication service error' }); } } /** * Local JWT authentication (existing system) */ async authenticateLocal(req, res, next) { const authHeader = req.headers['authorization']; const token = authHeader && authHeader.split(' ')[1]; if (!token) { return res.status(401).json({ success: false, message: 'Access token required' }); } try { const decoded = jwt.verify(token, process.env.JWT_SECRET); const UserModel = this.models ? this.models.User : User; const user = await UserModel.findByPk(decoded.userId, { attributes: ['id', 'username', 'email', 'role', 'is_active', 'tenant_id'] }); if (!user || !user.is_active) { return res.status(401).json({ success: false, message: 'Invalid or inactive user' }); } // Verify user belongs to tenant if (user.tenant_id !== req.tenant.id) { return res.status(403).json({ success: false, message: 'User not authorized for this tenant' }); } req.user = user; next(); } catch (error) { console.error('JWT verification error:', error); return res.status(403).json({ success: false, message: 'Invalid or expired token' }); } } /** * SAML authentication for Active Directory */ async authenticateSAML(req, res, next) { const provider = this.providers.get(AuthProviders.SAML); return provider.authenticate(req, res, next); } /** * OAuth 2.0/OpenID Connect authentication */ async authenticateOAuth(req, res, next) { const provider = this.providers.get(AuthProviders.OAUTH); return provider.authenticate(req, res, next); } /** * LDAP authentication */ async authenticateLDAP(req, res, next) { const provider = this.providers.get(AuthProviders.LDAP); return provider.authenticate(req, res, next); } /** * Create or update user from external provider */ async createOrUpdateExternalUser(tenantId, externalUser, authConfig) { const userMapping = authConfig.userMapping; const roleMapping = authConfig.roleMapping; // Map external user attributes to internal user fields const userData = { username: this.getAttributeValue(externalUser, userMapping.username), email: this.getAttributeValue(externalUser, userMapping.email), first_name: this.getAttributeValue(externalUser, userMapping.firstName), last_name: this.getAttributeValue(externalUser, userMapping.lastName), phone_number: this.getAttributeValue(externalUser, userMapping.phoneNumber), tenant_id: tenantId, is_active: true, external_provider: authConfig.type, external_id: externalUser.id || externalUser.sub || externalUser.nameID, last_login: new Date() }; // Map roles from external provider const externalRoles = externalUser.roles || externalUser.groups || []; userData.role = this.mapExternalRole(externalRoles, roleMapping); // Find or create user const [user, created] = await User.findOrCreate({ where: { external_id: userData.external_id, tenant_id: tenantId }, defaults: userData }); // Update existing user if (!created) { await user.update(userData); } return user; } /** * Get attribute value from external user object */ getAttributeValue(externalUser, attributeNames) { if (!Array.isArray(attributeNames)) { attributeNames = [attributeNames]; } for (const attrName of attributeNames) { if (externalUser[attrName]) { return externalUser[attrName]; } } return null; } /** * Map external roles to internal roles */ mapExternalRole(externalRoles, roleMapping) { for (const externalRole of externalRoles) { if (roleMapping[externalRole]) { return roleMapping[externalRole]; } } return roleMapping.default || 'viewer'; } /** * Generate JWT token for authenticated user */ generateJWTToken(user, tenantId) { return jwt.sign( { userId: user.id, username: user.username, role: user.role, tenantId: tenantId, provider: user.external_provider || 'local' }, process.env.JWT_SECRET, { expiresIn: '24h' } ); } /** * Validate that a user has access to a specific tenant * @param {string} userId - The user ID * @param {string} tenantSlug - The tenant slug * @returns {boolean} - True if user has access to tenant */ async validateTenantAccess(userId, tenantSlug) { try { const { User, Tenant } = this.models || require('../models'); // Find the user const user = await User.findByPk(userId, { include: [{ model: Tenant, as: 'tenant' }] }); if (!user) { return false; } // Check if user's tenant matches the requested tenant return user.tenant && user.tenant.slug === tenantSlug; } catch (error) { console.error('Error validating tenant access:', error); return false; } } } module.exports = MultiTenantAuth;