Files
drone-detector/server/middleware/multi-tenant-auth.js
2025-09-15 14:20:22 +02:00

412 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;
}
/**
* 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 || '';
if (hostname && !hostname.startsWith('localhost')) {
const hostParts = hostname.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' && !subdomain.includes(':')) {
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');
console.log('🔍 validateTenantAccess called with:', { userId, tenantSlug });
// Find the user
const user = await User.findByPk(userId, {
include: [{
model: Tenant,
as: 'tenant'
}]
});
console.log('🔍 Found user:', user ? {
id: user.id,
username: user.username,
tenant_id: user.tenant_id,
tenant: user.tenant ? {
id: user.tenant.id,
slug: user.tenant.slug,
name: user.tenant.name
} : null
} : null);
if (!user) {
console.log('❌ User not found');
return false;
}
// Check if user's tenant matches the requested tenant
const result = user.tenant && user.tenant.slug === tenantSlug;
console.log('🔍 Validation result:', {
userTenantSlug: user.tenant?.slug,
requestedSlug: tenantSlug,
result
});
return result;
} catch (error) {
console.error('Error validating tenant access:', error);
return false;
}
}
}
module.exports = MultiTenantAuth;