404 lines
12 KiB
JavaScript
404 lines
12 KiB
JavaScript
/**
|
|
* 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) {
|
|
console.log('🚀 DETERMINE TENANT FUNCTION START');
|
|
console.log('===== DETERMINE TENANT CALLED =====');
|
|
console.log('🏢 req.user:', req.user);
|
|
console.log('🏢 req.headers.host:', req.headers?.host);
|
|
console.log('🏢 req.url:', req.url);
|
|
console.log('🏢 req.path:', req.path);
|
|
|
|
// Method 1: From authenticated user (highest priority)
|
|
if (req.user && req.user.tenantId) {
|
|
console.log('🏢 Tenant from req.user.tenantId:', 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'];
|
|
console.log('🏢 x-forwarded-host header:', forwardedHost);
|
|
if (forwardedHost) {
|
|
const subdomain = forwardedHost.split('.')[0];
|
|
if (subdomain && subdomain !== 'www' && subdomain !== 'api' && !subdomain.includes(':')) {
|
|
console.log('🏢 Tenant from x-forwarded-host:', subdomain);
|
|
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') {
|
|
console.log('🏢 Tenant from subdomain:', subdomain);
|
|
return subdomain;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 6: URL path (/tenant2/api/...)
|
|
const urlPath = req.path || req.url || '';
|
|
console.log('🏢 Raw urlPath value:', urlPath);
|
|
const pathSegments = urlPath.split('/').filter(segment => segment);
|
|
console.log('🏢 URL path segments:', pathSegments, 'from path:', req.path, 'or url:', req.url);
|
|
if (pathSegments.length > 0 && pathSegments[0] !== 'api') {
|
|
console.log('🏢 Tenant from URL path:', pathSegments[0]);
|
|
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')) {
|
|
console.log('🏢 Localhost detected, returning null');
|
|
return null;
|
|
}
|
|
|
|
console.log('🏢 No tenant determined, returning 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;
|