/** * Multi-Tenant Authentication Middleware * Routes authentication requests to appropriate providers */ 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(); // Initialize authentication providers this.initializeProviders(); } /** * 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: Subdomain (tenant.yourapp.com) const subdomain = req.hostname.split('.')[0]; if (subdomain && subdomain !== 'www' && subdomain !== 'api') { return subdomain; } // Method 2: Custom header const tenantHeader = req.headers['x-tenant-id']; if (tenantHeader) { return tenantHeader; } // Method 3: 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); return decoded.tenantId; } catch (error) { // Token invalid, continue with other methods } } // Method 4: Query parameter (for redirects) if (req.query.tenant) { return req.query.tenant; } // Default to 'default' tenant for backward compatibility return 'default'; } /** * Get authentication configuration for tenant */ async getTenantAuthConfig(tenantId) { const tenant = await Tenant.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); const authConfig = await this.getTenantAuthConfig(tenantId); // Attach tenant info to request req.tenant = { id: tenantId, authConfig }; // Route to appropriate authentication provider switch (authConfig.type) { case AuthProviders.LOCAL: return this.authenticateLocal(req, res, next); case AuthProviders.SAML: return this.authenticateSAML(req, res, next); case AuthProviders.OAUTH: return this.authenticateOAuth(req, res, next); case AuthProviders.LDAP: return this.authenticateLDAP(req, res, next); default: return this.authenticateLocal(req, res, 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 user = await User.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' } ); } /** * Determine tenant from request (slug or subdomain) */ async determineTenant(req) { try { // Try to get tenant from subdomain first const host = req.get('host') || ''; const subdomain = host.split('.')[0]; // If subdomain is not localhost/IP, use it as tenant slug if (subdomain && !subdomain.match(/^(localhost|127\.0\.0\.1|\d+\.\d+\.\d+\.\d+)$/)) { return subdomain; } // Fallback: get from user's tenant if authenticated if (req.user && req.user.tenant_id) { const tenant = await Tenant.findByPk(req.user.tenant_id); return tenant ? tenant.slug : null; } return null; } catch (error) { console.error('Error determining tenant:', error); return null; } } /** * 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 } = 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;