diff --git a/client/src/contexts/MultiTenantAuthContext.jsx b/client/src/contexts/MultiTenantAuthContext.jsx
new file mode 100644
index 0000000..0ff10b5
--- /dev/null
+++ b/client/src/contexts/MultiTenantAuthContext.jsx
@@ -0,0 +1,255 @@
+/**
+ * Multi-Tenant Authentication Context
+ * Handles authentication for different tenants and providers
+ */
+
+import React, { createContext, useContext, useReducer, useEffect } from 'react';
+import api from '../services/api';
+
+const MultiTenantAuthContext = createContext();
+
+const authReducer = (state, action) => {
+ switch (action.type) {
+ case 'SET_LOADING':
+ return { ...state, loading: action.payload };
+ case 'SET_TENANT':
+ return { ...state, tenant: action.payload };
+ case 'SET_AUTH_CONFIG':
+ return { ...state, authConfig: action.payload };
+ case 'LOGIN_START':
+ return { ...state, loading: true, error: null };
+ case 'LOGIN_SUCCESS':
+ return {
+ ...state,
+ user: action.payload.user,
+ token: action.payload.token,
+ tenant: action.payload.tenant,
+ isAuthenticated: true,
+ loading: false,
+ error: null
+ };
+ case 'LOGIN_FAILURE':
+ return { ...state, loading: false, error: action.payload, isAuthenticated: false };
+ case 'LOGOUT':
+ return { ...state, user: null, token: null, isAuthenticated: false, loading: false };
+ case 'SET_ERROR':
+ return { ...state, error: action.payload, loading: false };
+ default:
+ return state;
+ }
+};
+
+const initialState = {
+ user: null,
+ token: localStorage.getItem('token'),
+ tenant: null,
+ authConfig: null,
+ isAuthenticated: false,
+ loading: true,
+ error: null
+};
+
+export const MultiTenantAuthProvider = ({ children }) => {
+ const [state, dispatch] = useReducer(authReducer, initialState);
+
+ // Determine tenant from URL or storage
+ const determineTenant = () => {
+ // Method 1: Subdomain
+ const hostname = window.location.hostname;
+ const subdomain = hostname.split('.')[0];
+ if (subdomain && subdomain !== 'www' && subdomain !== 'localhost' && !hostname.includes('127.0.0.1')) {
+ return subdomain;
+ }
+
+ // Method 2: URL parameter
+ const urlParams = new URLSearchParams(window.location.search);
+ if (urlParams.get('tenant')) {
+ return urlParams.get('tenant');
+ }
+
+ // Method 3: Local storage
+ const storedTenant = localStorage.getItem('tenant');
+ if (storedTenant) {
+ return storedTenant;
+ }
+
+ // Default tenant
+ return 'default';
+ };
+
+ // Initialize authentication
+ useEffect(() => {
+ const initAuth = async () => {
+ try {
+ const tenantId = determineTenant();
+ dispatch({ type: 'SET_TENANT', payload: tenantId });
+ localStorage.setItem('tenant', tenantId);
+
+ // Get tenant auth configuration
+ const authConfigResponse = await api.get(`/auth/config/${tenantId}`);
+ dispatch({ type: 'SET_AUTH_CONFIG', payload: authConfigResponse.data.data });
+
+ // Check if user is already authenticated
+ const token = localStorage.getItem('token');
+ if (token) {
+ // Validate token with current tenant context
+ const profileResponse = await api.get('/users/profile');
+ dispatch({
+ type: 'LOGIN_SUCCESS',
+ payload: {
+ user: profileResponse.data.data,
+ token: token,
+ tenant: tenantId
+ }
+ });
+ } else {
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ } catch (error) {
+ console.error('Auth initialization error:', error);
+ localStorage.removeItem('token');
+ dispatch({ type: 'SET_LOADING', payload: false });
+ }
+ };
+
+ initAuth();
+ }, []);
+
+ // Handle URL token (from SSO redirects)
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const urlToken = urlParams.get('token');
+ const urlTenant = urlParams.get('tenant');
+
+ if (urlToken && urlTenant) {
+ localStorage.setItem('token', urlToken);
+ localStorage.setItem('tenant', urlTenant);
+
+ // Clean URL
+ window.history.replaceState({}, document.title, window.location.pathname);
+
+ // Reload to reinitialize with new token
+ window.location.reload();
+ }
+ }, []);
+
+ // Login function
+ const login = async (credentials) => {
+ try {
+ dispatch({ type: 'LOGIN_START' });
+
+ const tenantId = state.tenant || determineTenant();
+
+ // For local authentication, use existing login endpoint
+ if (state.authConfig?.provider === 'local' || state.authConfig?.provider === 'ldap') {
+ const response = await api.post('/auth/login', {
+ ...credentials,
+ tenant: tenantId
+ });
+
+ const { user, token } = response.data.data;
+ localStorage.setItem('token', token);
+ localStorage.setItem('tenant', tenantId);
+
+ dispatch({
+ type: 'LOGIN_SUCCESS',
+ payload: { user, token, tenant: tenantId }
+ });
+
+ return { success: true };
+ } else {
+ // For SSO providers, redirect to SSO endpoint
+ const ssoUrl = getSSOLoginUrl(state.authConfig, tenantId);
+ window.location.href = ssoUrl;
+ return { success: true, redirect: true };
+ }
+ } catch (error) {
+ const errorMessage = error.response?.data?.message || 'Login failed';
+ dispatch({ type: 'LOGIN_FAILURE', payload: errorMessage });
+ return { success: false, error: errorMessage };
+ }
+ };
+
+ // SSO Login
+ const loginWithSSO = () => {
+ const tenantId = state.tenant || determineTenant();
+ const ssoUrl = getSSOLoginUrl(state.authConfig, tenantId);
+ window.location.href = ssoUrl;
+ };
+
+ // Get SSO login URL
+ const getSSOLoginUrl = (authConfig, tenantId) => {
+ const returnUrl = encodeURIComponent(window.location.pathname + window.location.search);
+
+ switch (authConfig?.provider) {
+ case 'saml':
+ return `/auth/saml/${tenantId}/login?returnUrl=${returnUrl}`;
+ case 'oauth':
+ return `/auth/oauth/${tenantId}/login?returnUrl=${returnUrl}`;
+ default:
+ return `/login?tenant=${tenantId}`;
+ }
+ };
+
+ // Logout
+ const logout = async () => {
+ try {
+ const tenantId = state.tenant;
+
+ // Call logout endpoint
+ const response = await api.post('/auth/logout');
+
+ // Clear local storage
+ localStorage.removeItem('token');
+ localStorage.removeItem('tenant');
+
+ dispatch({ type: 'LOGOUT' });
+
+ // Handle provider-specific logout
+ if (response.data.logout_url) {
+ window.location.href = response.data.logout_url;
+ } else {
+ window.location.href = `/login?tenant=${tenantId}`;
+ }
+ } catch (error) {
+ console.error('Logout error:', error);
+ // Force logout even if API call fails
+ localStorage.removeItem('token');
+ localStorage.removeItem('tenant');
+ dispatch({ type: 'LOGOUT' });
+ window.location.href = '/login';
+ }
+ };
+
+ // Switch tenant
+ const switchTenant = (newTenantId) => {
+ localStorage.setItem('tenant', newTenantId);
+ localStorage.removeItem('token'); // Clear token when switching tenants
+ window.location.href = `/login?tenant=${newTenantId}`;
+ };
+
+ const value = {
+ ...state,
+ login,
+ logout,
+ loginWithSSO,
+ switchTenant,
+ getSSOLoginUrl
+ };
+
+ return (
+
+ {children}
+
+ );
+};
+
+export const useMultiTenantAuth = () => {
+ const context = useContext(MultiTenantAuthContext);
+ if (!context) {
+ throw new Error('useMultiTenantAuth must be used within a MultiTenantAuthProvider');
+ }
+ return context;
+};
+
+export default MultiTenantAuthContext;
diff --git a/client/src/pages/MultiTenantLogin.jsx b/client/src/pages/MultiTenantLogin.jsx
new file mode 100644
index 0000000..03f45a0
--- /dev/null
+++ b/client/src/pages/MultiTenantLogin.jsx
@@ -0,0 +1,305 @@
+/**
+ * Enhanced Login Component with Multi-Tenant Support
+ * Handles different authentication providers based on tenant configuration
+ */
+
+import React, { useState, useEffect } from 'react';
+import { Navigate, useSearchParams } from 'react-router-dom';
+import { useMultiTenantAuth } from '../contexts/MultiTenantAuthContext';
+import { EyeIcon, EyeSlashIcon } from '@heroicons/react/24/outline';
+import toast from 'react-hot-toast';
+
+const MultiTenantLogin = () => {
+ const [searchParams] = useSearchParams();
+ const [credentials, setCredentials] = useState({
+ username: '',
+ password: ''
+ });
+ const [showPassword, setShowPassword] = useState(false);
+ const {
+ login,
+ loginWithSSO,
+ loading,
+ isAuthenticated,
+ tenant,
+ authConfig,
+ error
+ } = useMultiTenantAuth();
+
+ const tenantParam = searchParams.get('tenant');
+ const errorParam = searchParams.get('error');
+
+ useEffect(() => {
+ // Handle SSO errors from URL parameters
+ if (errorParam) {
+ const errorMessages = {
+ 'auth_failed': 'Authentication failed. Please try again.',
+ 'auth_cancelled': 'Authentication was cancelled.',
+ 'user_creation_failed': 'Failed to create user account.',
+ 'saml_error': 'SAML authentication error.',
+ 'oauth_error': 'OAuth authentication error.'
+ };
+
+ toast.error(errorMessages[errorParam] || 'Authentication error occurred.');
+ }
+ }, [errorParam]);
+
+ if (isAuthenticated) {
+ return ;
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ if (!credentials.username || !credentials.password) {
+ toast.error('Please fill in all fields');
+ return;
+ }
+
+ const result = await login(credentials);
+
+ if (result.success && !result.redirect) {
+ toast.success('Login successful!');
+ } else if (!result.success) {
+ toast.error(result.error || 'Login failed');
+ }
+ // If redirect is true, the page will redirect to SSO provider
+ };
+
+ const handleChange = (e) => {
+ setCredentials({
+ ...credentials,
+ [e.target.name]: e.target.value
+ });
+ };
+
+ const handleSSOLogin = () => {
+ loginWithSSO();
+ };
+
+ const renderAuthenticationMethod = () => {
+ if (!authConfig) {
+ return (
+
+
+
Loading authentication configuration...
+
+ );
+ }
+
+ const { provider, features } = authConfig;
+
+ // SSO-only providers
+ if (provider === 'saml' || provider === 'oauth') {
+ return (
+
+
+
+ Single Sign-On Required
+
+
+ Please sign in using your organization's identity provider.
+
+
+
+
+
+ {tenant && tenant !== 'default' && (
+
+
+ Organization: {tenant}
+
+
+ )}
+
+ );
+ }
+
+ // Local or LDAP authentication with form
+ return (
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ Drone Detection System
+
+
+ Sign in to your account
+
+
+ {/* Tenant indicator */}
+ {tenant && tenant !== 'default' && (
+
+
+ Organization: {tenant}
+
+
+ )}
+
+
+ {error && (
+
+ )}
+
+ {renderAuthenticationMethod()}
+
+ {/* Tenant switcher for development */}
+ {process.env.NODE_ENV === 'development' && (
+
+
+ Development Options
+
+
+
+
+
+
+
+ )}
+
+
+ );
+};
+
+export default MultiTenantLogin;
diff --git a/docs/MULTI_TENANT_AUTH_SETUP.md b/docs/MULTI_TENANT_AUTH_SETUP.md
new file mode 100644
index 0000000..ad4855e
--- /dev/null
+++ b/docs/MULTI_TENANT_AUTH_SETUP.md
@@ -0,0 +1,291 @@
+# Multi-Tenant Authentication Setup Guide
+
+## Overview
+
+This guide explains how to configure your UAV Detection System for multi-tenant authentication with support for various identity providers including Active Directory, SAML, OAuth, and LDAP.
+
+## ๐๏ธ Architecture
+
+### Tenant Isolation
+- **Subdomain-based**: `customer1.yourapp.com`, `customer2.yourapp.com`
+- **Header-based**: `X-Tenant-ID: customer1`
+- **JWT-based**: Token contains tenant information
+
+### Supported Authentication Providers
+1. **Local JWT** (default)
+2. **SAML 2.0** (Active Directory/ADFS)
+3. **OAuth 2.0/OpenID Connect** (Azure AD, Google, etc.)
+4. **LDAP** (Direct Active Directory)
+5. **Custom SSO**
+
+## ๐ฆ Required Dependencies
+
+Add these to your `package.json`:
+
+```json
+{
+ "dependencies": {
+ "passport": "^0.6.0",
+ "passport-saml": "^3.2.4",
+ "passport-oauth2": "^1.7.0",
+ "passport-openidconnect": "^0.1.1",
+ "ldapjs": "^3.0.3",
+ "express-session": "^1.17.3"
+ }
+}
+```
+
+## ๐ ๏ธ Installation
+
+### 1. Install Dependencies
+```bash
+npm install passport passport-saml passport-oauth2 passport-openidconnect ldapjs express-session
+```
+
+### 2. Database Migration
+```bash
+# Create tenant table and update user table
+npm run migrate
+```
+
+### 3. Environment Variables
+```env
+# Multi-tenant configuration
+BASE_URL=https://yourapp.com
+DOMAIN_NAME=yourapp.com
+SESSION_SECRET=your-session-secret-key
+
+# Default tenant (backward compatibility)
+DEFAULT_TENANT_ID=default
+```
+
+### 4. Update Server Configuration
+
+In `server/index.js`, add the auth routes:
+
+```javascript
+// Add multi-tenant auth routes
+app.use('/auth', require('./routes/auth'));
+app.use('/api/tenants', require('./routes/tenants'));
+
+// Update existing auth middleware
+const MultiTenantAuth = require('./middleware/multi-tenant-auth');
+const multiAuth = new MultiTenantAuth();
+
+// Replace existing authenticateToken with multi-tenant version
+app.use('/api', (req, res, next) => {
+ multiAuth.authenticate(req, res, next);
+});
+```
+
+## ๐ง Configuration Examples
+
+### 1. Active Directory SAML Integration
+
+```javascript
+// Tenant configuration for SAML
+const tenantConfig = {
+ name: "Acme Corporation",
+ slug: "acme",
+ auth_provider: "saml",
+ auth_config: {
+ sso_url: "https://adfs.acme.com/adfs/ls/",
+ certificate: "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----",
+ issuer: "urn:acme:uav-detection",
+ logout_url: "https://adfs.acme.com/adfs/ls/?wa=wsignout1.0"
+ },
+ role_mapping: {
+ "Domain Admins": "admin",
+ "UAV-Operators": "operator",
+ "UAV-Viewers": "viewer",
+ "default": "viewer"
+ }
+};
+```
+
+### 2. Azure AD OAuth Integration
+
+```javascript
+// Tenant configuration for OAuth (Azure AD)
+const tenantConfig = {
+ name: "TechCorp",
+ slug: "techcorp",
+ auth_provider: "oauth",
+ auth_config: {
+ client_id: "your-azure-app-id",
+ client_secret: "your-azure-app-secret",
+ authorization_url: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/authorize",
+ token_url: "https://login.microsoftonline.com/{tenant-id}/oauth2/v2.0/token",
+ userinfo_url: "https://graph.microsoft.com/v1.0/me",
+ scopes: ["openid", "profile", "email", "User.Read"]
+ },
+ user_mapping: {
+ username: ["preferred_username", "userPrincipalName"],
+ email: ["mail", "email"],
+ firstName: ["givenName"],
+ lastName: ["surname"]
+ }
+};
+```
+
+### 3. Direct LDAP Integration
+
+```javascript
+// Tenant configuration for LDAP
+const tenantConfig = {
+ name: "Enterprise Corp",
+ slug: "enterprise",
+ auth_provider: "ldap",
+ auth_config: {
+ url: "ldaps://dc.enterprise.com:636",
+ base_dn: "dc=enterprise,dc=com",
+ bind_dn: "cn=service-account,ou=service-accounts,dc=enterprise,dc=com",
+ bind_password: "service-account-password",
+ user_search_filter: "(sAMAccountName={username})",
+ domain: "ENTERPRISE"
+ }
+};
+```
+
+## ๐ API Usage
+
+### Create Tenant
+```javascript
+POST /api/tenants
+{
+ "name": "Customer Company",
+ "slug": "customer",
+ "auth_provider": "saml",
+ "auth_config": { ... },
+ "subscription_type": "premium"
+}
+```
+
+### Login Flow
+```javascript
+// 1. Get tenant auth config
+GET /auth/config/customer
+
+// 2. For SSO providers, redirect to:
+GET /auth/saml/customer/login
+GET /auth/oauth/customer/login
+
+// 3. For local/LDAP, use:
+POST /auth/login
+{
+ "username": "user@customer.com",
+ "password": "password"
+}
+```
+
+### Frontend Integration
+```javascript
+// Determine authentication method
+const authConfig = await fetch(`/auth/config/${tenantId}`);
+
+if (authConfig.provider === 'saml') {
+ // Redirect to SAML login
+ window.location.href = `/auth/saml/${tenantId}/login`;
+} else {
+ // Show login form for local/LDAP
+ showLoginForm();
+}
+```
+
+## ๐ Security Considerations
+
+### 1. Certificate Management
+- Store SAML certificates securely
+- Rotate certificates regularly
+- Validate certificate chains
+
+### 2. Tenant Isolation
+- Ensure users can only access their tenant's data
+- Validate tenant context in all API calls
+- Implement tenant-specific rate limiting
+
+### 3. Configuration Security
+- Encrypt sensitive configuration data
+- Use environment variables for secrets
+- Implement configuration validation
+
+## ๐ Deployment Scenarios
+
+### SaaS Deployment
+```nginx
+# Nginx configuration for subdomain routing
+server {
+ server_name *.yourapp.com;
+
+ location / {
+ proxy_pass http://app-server;
+ proxy_set_header Host $host;
+ proxy_set_header X-Tenant-ID $subdomain;
+ }
+}
+```
+
+### On-Premise Deployment
+```yaml
+# Docker Compose for on-premise
+version: '3.8'
+services:
+ app:
+ environment:
+ - DEFAULT_TENANT_ID=onprem
+ - AUTH_PROVIDER=ldap
+ - LDAP_URL=ldap://company-dc:389
+```
+
+## ๐งช Testing
+
+### Test Authentication Configuration
+```bash
+# Test LDAP connection
+curl -X POST /auth/test/customer \
+ -H "Content-Type: application/json" \
+ -H "Authorization: Bearer admin-token"
+
+# Test SAML metadata
+curl /auth/saml/customer/metadata
+```
+
+### User Creation Flow
+1. User authenticates with external provider
+2. System maps external attributes to internal user
+3. User record created/updated with tenant association
+4. JWT issued with tenant context
+
+## ๐ Migration Strategy
+
+### Existing Customers
+1. Create default tenant for existing users
+2. Gradually migrate to tenant-specific authentication
+3. Maintain backward compatibility during transition
+
+### Data Migration
+```sql
+-- Create default tenant
+INSERT INTO tenants (name, slug, auth_provider)
+VALUES ('Default', 'default', 'local');
+
+-- Associate existing users with default tenant
+UPDATE users SET tenant_id = (
+ SELECT id FROM tenants WHERE slug = 'default'
+) WHERE tenant_id IS NULL;
+```
+
+## ๐ Support
+
+### Common Issues
+1. **SAML Certificate Errors**: Verify certificate format and validity
+2. **LDAP Connection Failures**: Check network connectivity and credentials
+3. **OAuth Scope Issues**: Ensure required scopes are granted
+4. **Tenant Not Found**: Verify tenant slug and domain configuration
+
+### Monitoring
+- Log authentication attempts and failures
+- Monitor tenant-specific metrics
+- Alert on authentication service health
+
+This architecture provides a scalable foundation for both SaaS and on-premise deployments while maintaining security and user experience.
diff --git a/server/config/auth-providers.js b/server/config/auth-providers.js
new file mode 100644
index 0000000..1896659
--- /dev/null
+++ b/server/config/auth-providers.js
@@ -0,0 +1,113 @@
+/**
+ * Authentication Providers Configuration
+ * Supports multiple auth strategies for SaaS and on-premise deployments
+ */
+
+const AuthProviders = {
+ // Local JWT authentication (default)
+ LOCAL: 'local',
+
+ // SAML 2.0 for Active Directory/ADFS
+ SAML: 'saml',
+
+ // OAuth 2.0/OpenID Connect
+ OAUTH: 'oauth',
+
+ // LDAP for on-premise AD
+ LDAP: 'ldap',
+
+ // Custom SSO
+ CUSTOM_SSO: 'custom_sso'
+};
+
+/**
+ * Tenant-specific authentication configuration
+ * Each tenant can have different auth providers
+ */
+class AuthConfig {
+ constructor() {
+ this.providers = new Map();
+ this.defaultProvider = AuthProviders.LOCAL;
+ }
+
+ /**
+ * Register authentication provider for a tenant
+ * @param {string} tenantId - Tenant identifier
+ * @param {object} config - Provider configuration
+ */
+ registerProvider(tenantId, config) {
+ this.providers.set(tenantId, {
+ type: config.type,
+ enabled: config.enabled || true,
+ config: config.settings,
+ userMapping: config.userMapping || this.getDefaultUserMapping(),
+ roleMapping: config.roleMapping || this.getDefaultRoleMapping(),
+ createdAt: new Date()
+ });
+ }
+
+ /**
+ * Get authentication provider for tenant
+ * @param {string} tenantId - Tenant identifier
+ * @returns {object} Provider configuration
+ */
+ getProvider(tenantId) {
+ return this.providers.get(tenantId) || {
+ type: this.defaultProvider,
+ enabled: true,
+ config: {},
+ userMapping: this.getDefaultUserMapping(),
+ roleMapping: this.getDefaultRoleMapping()
+ };
+ }
+
+ /**
+ * Default user attribute mapping from external providers
+ */
+ getDefaultUserMapping() {
+ return {
+ username: ['preferred_username', 'samAccountName', 'username', 'sub'],
+ email: ['email', 'mail', 'emailAddress'],
+ firstName: ['given_name', 'givenName', 'firstName'],
+ lastName: ['family_name', 'surname', 'lastName'],
+ displayName: ['name', 'displayName', 'cn'],
+ phoneNumber: ['phone_number', 'telephoneNumber', 'mobile']
+ };
+ }
+
+ /**
+ * Default role mapping from external providers to internal roles
+ */
+ getDefaultRoleMapping() {
+ return {
+ // Active Directory groups to internal roles
+ 'Domain Admins': 'admin',
+ 'UAV-Admins': 'admin',
+ 'UAV-Operators': 'operator',
+ 'UAV-Viewers': 'viewer',
+
+ // OAuth/SAML role claims
+ 'admin': 'admin',
+ 'operator': 'operator',
+ 'viewer': 'viewer',
+
+ // Default fallback
+ 'default': 'viewer'
+ };
+ }
+
+ /**
+ * Get all configured providers
+ */
+ getAllProviders() {
+ return Array.from(this.providers.entries()).map(([tenantId, config]) => ({
+ tenantId,
+ ...config
+ }));
+ }
+}
+
+module.exports = {
+ AuthProviders,
+ AuthConfig
+};
diff --git a/server/middleware/ldap-auth.js b/server/middleware/ldap-auth.js
new file mode 100644
index 0000000..4f360a2
--- /dev/null
+++ b/server/middleware/ldap-auth.js
@@ -0,0 +1,325 @@
+/**
+ * LDAP Authentication Provider
+ * Direct integration with Active Directory LDAP
+ */
+
+const ldap = require('ldapjs');
+const crypto = require('crypto');
+
+class LDAPAuth {
+ constructor() {
+ this.connections = new Map();
+ }
+
+ /**
+ * Create LDAP client connection
+ */
+ createLDAPClient(config) {
+ const clientOptions = {
+ url: config.url, // ldap://dc.company.com:389 or ldaps://dc.company.com:636
+ timeout: config.timeout || 5000,
+ connectTimeout: config.connect_timeout || 10000,
+ tlsOptions: {
+ rejectUnauthorized: config.tls_reject_unauthorized !== false
+ }
+ };
+
+ if (config.ca_cert) {
+ clientOptions.tlsOptions.ca = [config.ca_cert];
+ }
+
+ return ldap.createClient(clientOptions);
+ }
+
+ /**
+ * Authenticate user against LDAP
+ */
+ async authenticateUser(tenantId, username, password, config) {
+ return new Promise((resolve, reject) => {
+ const client = this.createLDAPClient(config);
+
+ // Build user DN
+ const userDN = this.buildUserDN(username, config);
+
+ client.bind(userDN, password, (err) => {
+ if (err) {
+ client.unbind();
+ return reject(new Error('Invalid credentials'));
+ }
+
+ // Search for user details
+ this.searchUser(client, username, config)
+ .then(userInfo => {
+ client.unbind();
+ resolve(userInfo);
+ })
+ .catch(searchErr => {
+ client.unbind();
+ reject(searchErr);
+ });
+ });
+ });
+ }
+
+ /**
+ * Build user Distinguished Name (DN)
+ */
+ buildUserDN(username, config) {
+ // Method 1: UPN format (user@domain.com)
+ if (config.user_dn_format === 'upn') {
+ return `${username}@${config.domain}`;
+ }
+
+ // Method 2: CN format (CN=username,OU=Users,DC=domain,DC=com)
+ if (config.user_dn_template) {
+ return config.user_dn_template.replace('{username}', username);
+ }
+
+ // Method 3: sAMAccountName format (DOMAIN\\username)
+ if (config.domain) {
+ return `${config.domain}\\${username}`;
+ }
+
+ // Default: assume username is already a DN
+ return username;
+ }
+
+ /**
+ * Search for user information in LDAP
+ */
+ async searchUser(client, username, config) {
+ return new Promise((resolve, reject) => {
+ const baseDN = config.base_dn || 'dc=example,dc=com';
+ const searchFilter = config.user_search_filter || `(sAMAccountName=${username})`;
+
+ const searchOptions = {
+ filter: searchFilter.replace('{username}', username),
+ scope: 'sub',
+ attributes: [
+ 'sAMAccountName',
+ 'userPrincipalName',
+ 'mail',
+ 'givenName',
+ 'sn',
+ 'displayName',
+ 'telephoneNumber',
+ 'memberOf',
+ 'objectClass',
+ 'distinguishedName'
+ ]
+ };
+
+ client.search(baseDN, searchOptions, (err, searchRes) => {
+ if (err) {
+ return reject(err);
+ }
+
+ let userEntry = null;
+
+ searchRes.on('searchEntry', (entry) => {
+ userEntry = entry.object;
+ });
+
+ searchRes.on('error', (err) => {
+ reject(err);
+ });
+
+ searchRes.on('end', (result) => {
+ if (!userEntry) {
+ return reject(new Error('User not found in directory'));
+ }
+
+ // Extract user information
+ const userInfo = {
+ id: userEntry.distinguishedName,
+ username: userEntry.sAMAccountName || userEntry.userPrincipalName,
+ email: userEntry.mail,
+ firstName: userEntry.givenName,
+ lastName: userEntry.sn,
+ displayName: userEntry.displayName,
+ phoneNumber: userEntry.telephoneNumber,
+ groups: this.extractGroups(userEntry.memberOf),
+ distinguishedName: userEntry.distinguishedName,
+ raw: userEntry
+ };
+
+ resolve(userInfo);
+ });
+ });
+ });
+ }
+
+ /**
+ * Extract group names from memberOf attribute
+ */
+ extractGroups(memberOf) {
+ if (!memberOf) {
+ return [];
+ }
+
+ const groups = Array.isArray(memberOf) ? memberOf : [memberOf];
+
+ return groups.map(dn => {
+ // Extract CN from DN (e.g., "CN=Domain Admins,CN=Users,DC=domain,DC=com" -> "Domain Admins")
+ const cnMatch = dn.match(/^CN=([^,]+)/i);
+ return cnMatch ? cnMatch[1] : dn;
+ });
+ }
+
+ /**
+ * Test LDAP connection
+ */
+ async testConnection(config) {
+ return new Promise((resolve, reject) => {
+ const client = this.createLDAPClient(config);
+
+ const testDN = config.bind_dn || config.admin_dn;
+ const testPassword = config.bind_password || config.admin_password;
+
+ if (!testDN || !testPassword) {
+ client.unbind();
+ return reject(new Error('Admin credentials required for connection test'));
+ }
+
+ client.bind(testDN, testPassword, (err) => {
+ client.unbind();
+
+ if (err) {
+ reject(new Error(`LDAP connection failed: ${err.message}`));
+ } else {
+ resolve(true);
+ }
+ });
+ });
+ }
+
+ /**
+ * Handle LDAP authentication request
+ */
+ async authenticate(req, res, next) {
+ // LDAP authentication is typically used for login forms
+ // This middleware would be used in login POST endpoint
+
+ if (req.method !== 'POST' || !req.body.username || !req.body.password) {
+ return res.status(400).json({
+ success: false,
+ message: 'Username and password required for LDAP authentication'
+ });
+ }
+
+ try {
+ const tenantId = req.tenant.id;
+ const config = req.tenant.authConfig.config;
+ const { username, password } = req.body;
+
+ // Authenticate against LDAP
+ const userInfo = await this.authenticateUser(tenantId, username, password, config);
+
+ const { MultiTenantAuth } = require('./multi-tenant-auth');
+ const multiAuth = new MultiTenantAuth();
+
+ // Create or update user
+ const user = await multiAuth.createOrUpdateExternalUser(
+ tenantId,
+ userInfo,
+ req.tenant.authConfig
+ );
+
+ // Generate JWT token
+ const token = multiAuth.generateJWTToken(user, tenantId);
+
+ res.json({
+ success: true,
+ data: {
+ user: {
+ id: user.id,
+ username: user.username,
+ email: user.email,
+ role: user.role,
+ first_name: user.first_name,
+ last_name: user.last_name
+ },
+ token,
+ expires_in: '24h'
+ },
+ message: 'LDAP authentication successful'
+ });
+
+ } catch (error) {
+ console.error('LDAP authentication error:', error);
+
+ res.status(401).json({
+ success: false,
+ message: 'LDAP authentication failed',
+ error: process.env.NODE_ENV === 'development' ? error.message : 'Invalid credentials'
+ });
+ }
+ }
+
+ /**
+ * Sync users from LDAP directory
+ */
+ async syncUsers(tenantId, config) {
+ return new Promise((resolve, reject) => {
+ const client = this.createLDAPClient(config);
+
+ // Bind with admin credentials
+ const adminDN = config.bind_dn || config.admin_dn;
+ const adminPassword = config.bind_password || config.admin_password;
+
+ client.bind(adminDN, adminPassword, (err) => {
+ if (err) {
+ client.unbind();
+ return reject(new Error('Failed to bind with admin credentials'));
+ }
+
+ const baseDN = config.base_dn;
+ const userFilter = config.user_sync_filter || '(objectClass=user)';
+
+ const searchOptions = {
+ filter: userFilter,
+ scope: 'sub',
+ attributes: [
+ 'sAMAccountName', 'userPrincipalName', 'mail', 'givenName',
+ 'sn', 'displayName', 'telephoneNumber', 'memberOf', 'distinguishedName'
+ ]
+ };
+
+ const users = [];
+
+ client.search(baseDN, searchOptions, (err, searchRes) => {
+ if (err) {
+ client.unbind();
+ return reject(err);
+ }
+
+ searchRes.on('searchEntry', (entry) => {
+ const userEntry = entry.object;
+ users.push({
+ id: userEntry.distinguishedName,
+ username: userEntry.sAMAccountName,
+ email: userEntry.mail,
+ firstName: userEntry.givenName,
+ lastName: userEntry.sn,
+ displayName: userEntry.displayName,
+ phoneNumber: userEntry.telephoneNumber,
+ groups: this.extractGroups(userEntry.memberOf)
+ });
+ });
+
+ searchRes.on('error', (err) => {
+ client.unbind();
+ reject(err);
+ });
+
+ searchRes.on('end', () => {
+ client.unbind();
+ resolve(users);
+ });
+ });
+ });
+ });
+ }
+}
+
+module.exports = LDAPAuth;
diff --git a/server/middleware/multi-tenant-auth.js b/server/middleware/multi-tenant-auth.js
new file mode 100644
index 0000000..0d55cec
--- /dev/null
+++ b/server/middleware/multi-tenant-auth.js
@@ -0,0 +1,284 @@
+/**
+ * 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' }
+ );
+ }
+}
+
+module.exports = MultiTenantAuth;
diff --git a/server/middleware/oauth-auth.js b/server/middleware/oauth-auth.js
new file mode 100644
index 0000000..0a8fa79
--- /dev/null
+++ b/server/middleware/oauth-auth.js
@@ -0,0 +1,245 @@
+/**
+ * OAuth 2.0/OpenID Connect Authentication Provider
+ * Supports modern identity providers like Azure AD, Google, etc.
+ */
+
+const passport = require('passport');
+const OAuth2Strategy = require('passport-oauth2');
+const OpenIDConnectStrategy = require('passport-openidconnect');
+
+class OAuthAuth {
+ constructor() {
+ this.strategies = new Map();
+ }
+
+ /**
+ * Configure OAuth strategy for a tenant
+ */
+ configureOAuthStrategy(tenantId, config) {
+ const strategyName = `oauth-${tenantId}`;
+
+ // Determine if this is OpenID Connect or plain OAuth2
+ const isOpenIDConnect = config.discovery_url || config.issuer;
+
+ if (isOpenIDConnect) {
+ return this.configureOpenIDConnectStrategy(tenantId, config);
+ } else {
+ return this.configureOAuth2Strategy(tenantId, config);
+ }
+ }
+
+ /**
+ * Configure OpenID Connect strategy (recommended for modern providers)
+ */
+ configureOpenIDConnectStrategy(tenantId, config) {
+ const strategyName = `oidc-${tenantId}`;
+
+ const oidcOptions = {
+ issuer: config.issuer || config.discovery_url,
+ clientID: config.client_id,
+ clientSecret: config.client_secret,
+ callbackURL: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`,
+
+ // Scopes
+ scope: config.scopes || ['openid', 'profile', 'email'],
+
+ // Claims
+ skipUserProfile: false,
+
+ // Additional parameters
+ customHeaders: config.custom_headers || {},
+ passReqToCallback: true
+ };
+
+ const strategy = new OpenIDConnectStrategy(oidcOptions,
+ async (req, issuer, profile, accessToken, refreshToken, done) => {
+ try {
+ const externalUser = {
+ id: profile.id || profile.sub,
+ username: profile.preferred_username || profile.username || profile.email,
+ email: profile.email,
+ firstName: profile.given_name || profile.firstName,
+ lastName: profile.family_name || profile.lastName,
+ displayName: profile.name || profile.displayName,
+ roles: profile.roles || profile['custom:roles'] || [],
+ groups: profile.groups || profile['custom:groups'] || [],
+ raw: profile._json // Store raw profile for custom mapping
+ };
+
+ return done(null, { tenantId, externalUser, provider: 'oauth' });
+ } catch (error) {
+ return done(error, null);
+ }
+ });
+
+ passport.use(strategyName, strategy);
+ this.strategies.set(tenantId, { strategy, config, type: 'oidc' });
+
+ return strategyName;
+ }
+
+ /**
+ * Configure OAuth 2.0 strategy (for legacy providers)
+ */
+ configureOAuth2Strategy(tenantId, config) {
+ const strategyName = `oauth2-${tenantId}`;
+
+ const oauth2Options = {
+ authorizationURL: config.authorization_url,
+ tokenURL: config.token_url,
+ clientID: config.client_id,
+ clientSecret: config.client_secret,
+ callbackURL: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`,
+
+ // Custom parameters
+ customHeaders: config.custom_headers || {},
+ scope: config.scopes || ['profile', 'email'],
+ passReqToCallback: true
+ };
+
+ const strategy = new OAuth2Strategy(oauth2Options,
+ async (req, accessToken, refreshToken, profile, done) => {
+ try {
+ // Fetch user profile from userinfo endpoint
+ const userProfile = await this.fetchUserProfile(accessToken, config.userinfo_url);
+
+ const externalUser = {
+ id: userProfile.sub || userProfile.id,
+ username: userProfile.preferred_username || userProfile.username || userProfile.email,
+ email: userProfile.email,
+ firstName: userProfile.given_name || userProfile.first_name,
+ lastName: userProfile.family_name || userProfile.last_name,
+ displayName: userProfile.name || userProfile.display_name,
+ roles: userProfile.roles || [],
+ groups: userProfile.groups || [],
+ raw: userProfile
+ };
+
+ return done(null, { tenantId, externalUser, provider: 'oauth' });
+ } catch (error) {
+ return done(error, null);
+ }
+ });
+
+ passport.use(strategyName, strategy);
+ this.strategies.set(tenantId, { strategy, config, type: 'oauth2' });
+
+ return strategyName;
+ }
+
+ /**
+ * Fetch user profile from OAuth userinfo endpoint
+ */
+ async fetchUserProfile(accessToken, userinfoUrl) {
+ const axios = require('axios');
+
+ try {
+ const response = await axios.get(userinfoUrl, {
+ headers: {
+ 'Authorization': `Bearer ${accessToken}`,
+ 'Accept': 'application/json'
+ }
+ });
+
+ return response.data;
+ } catch (error) {
+ console.error('Error fetching user profile:', error);
+ throw new Error('Failed to fetch user profile');
+ }
+ }
+
+ /**
+ * Initiate OAuth authentication
+ */
+ async authenticate(req, res, next) {
+ const tenantId = req.tenant.id;
+
+ // Check if strategy is configured for this tenant
+ if (!this.strategies.has(tenantId)) {
+ const config = req.tenant.authConfig.config;
+ this.configureOAuthStrategy(tenantId, config);
+ }
+
+ const strategyInfo = this.strategies.get(tenantId);
+ const strategyName = strategyInfo.type === 'oidc' ? `oidc-${tenantId}` : `oauth2-${tenantId}`;
+
+ // Store return URL in session
+ req.session.returnUrl = req.query.returnUrl || '/';
+
+ // Use passport to authenticate
+ passport.authenticate(strategyName, {
+ state: req.query.returnUrl || '/'
+ })(req, res, next);
+ }
+
+ /**
+ * Handle OAuth callback
+ */
+ async handleCallback(req, res, next) {
+ const tenantId = req.params.tenantId;
+ const strategyInfo = this.strategies.get(tenantId);
+
+ if (!strategyInfo) {
+ return res.redirect(`/login?error=strategy_not_found&tenant=${tenantId}`);
+ }
+
+ const strategyName = strategyInfo.type === 'oidc' ? `oidc-${tenantId}` : `oauth2-${tenantId}`;
+
+ passport.authenticate(strategyName, async (err, authResult) => {
+ if (err) {
+ console.error('OAuth authentication error:', err);
+ return res.redirect(`/login?error=auth_failed&tenant=${tenantId}`);
+ }
+
+ if (!authResult) {
+ return res.redirect(`/login?error=auth_cancelled&tenant=${tenantId}`);
+ }
+
+ try {
+ const { MultiTenantAuth } = require('./multi-tenant-auth');
+ const multiAuth = new MultiTenantAuth();
+
+ // Create or update user
+ const user = await multiAuth.createOrUpdateExternalUser(
+ tenantId,
+ authResult.externalUser,
+ req.tenant.authConfig
+ );
+
+ // Generate JWT token
+ const token = multiAuth.generateJWTToken(user, tenantId);
+
+ // Redirect to application with token
+ const returnUrl = req.session.returnUrl || req.query.state || '/';
+ res.redirect(`${returnUrl}?token=${token}&tenant=${tenantId}`);
+
+ } catch (error) {
+ console.error('OAuth user creation error:', error);
+ return res.redirect(`/login?error=user_creation_failed&tenant=${tenantId}`);
+ }
+ })(req, res, next);
+ }
+
+ /**
+ * Get authorization URL for manual OAuth flow
+ */
+ getAuthorizationUrl(tenantId, returnUrl) {
+ const strategyInfo = this.strategies.get(tenantId);
+ if (!strategyInfo) {
+ throw new Error(`OAuth strategy not configured for tenant: ${tenantId}`);
+ }
+
+ const config = strategyInfo.config;
+ const params = new URLSearchParams({
+ client_id: config.client_id,
+ response_type: 'code',
+ redirect_uri: `${process.env.BASE_URL}/auth/oauth/${tenantId}/callback`,
+ scope: (config.scopes || ['openid', 'profile', 'email']).join(' '),
+ state: returnUrl || '/'
+ });
+
+ return `${config.authorization_url}?${params.toString()}`;
+ }
+}
+
+module.exports = OAuthAuth;
diff --git a/server/middleware/saml-auth.js b/server/middleware/saml-auth.js
new file mode 100644
index 0000000..fb40c6c
--- /dev/null
+++ b/server/middleware/saml-auth.js
@@ -0,0 +1,197 @@
+/**
+ * SAML Authentication Provider
+ * Supports Active Directory integration via ADFS
+ */
+
+const passport = require('passport');
+const SamlStrategy = require('passport-saml').Strategy;
+const fs = require('fs');
+const path = require('path');
+
+class SAMLAuth {
+ constructor() {
+ this.strategies = new Map();
+ }
+
+ /**
+ * Configure SAML strategy for a tenant
+ */
+ configureSAMLStrategy(tenantId, config) {
+ const strategyName = `saml-${tenantId}`;
+
+ const samlOptions = {
+ // SAML Service Provider (SP) Configuration
+ callbackUrl: `${process.env.BASE_URL}/auth/saml/${tenantId}/callback`,
+ entryPoint: config.sso_url, // ADFS SSO URL
+ issuer: config.issuer || `urn:${process.env.DOMAIN_NAME}:${tenantId}`,
+
+ // Identity Provider (IdP) Configuration
+ cert: config.certificate, // ADFS signing certificate
+
+ // Optional: Encryption
+ privateCert: config.private_key,
+ decryptionPvk: config.private_key,
+
+ // Attribute mapping
+ attributeConsumingServiceIndex: false,
+ disableRequestedAuthnContext: true,
+
+ // ADFS specific settings
+ identifierFormat: 'urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress',
+ acceptedClockSkewMs: 5000,
+
+ // Logout
+ logoutUrl: config.logout_url,
+ logoutCallbackUrl: `${process.env.BASE_URL}/auth/saml/${tenantId}/logout`
+ };
+
+ const strategy = new SamlStrategy(samlOptions, async (profile, done) => {
+ try {
+ // Extract user information from SAML assertion
+ const externalUser = {
+ id: profile.nameID,
+ username: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name'] || profile.nameID,
+ email: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] || profile.email,
+ firstName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/givenname'] || profile.givenName,
+ lastName: profile['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/surname'] || profile.surname,
+ displayName: profile['http://schemas.microsoft.com/ws/2008/06/identity/claims/windowsaccountname'] || profile.displayName,
+ groups: this.extractGroups(profile),
+ roles: this.extractRoles(profile)
+ };
+
+ return done(null, { tenantId, externalUser, provider: 'saml' });
+ } catch (error) {
+ return done(error, null);
+ }
+ });
+
+ passport.use(strategyName, strategy);
+ this.strategies.set(tenantId, { strategy, config });
+
+ return strategyName;
+ }
+
+ /**
+ * Extract Active Directory groups from SAML assertion
+ */
+ extractGroups(profile) {
+ const groupClaims = [
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/groups',
+ 'http://schemas.xmlsoap.org/claims/Group',
+ 'groups'
+ ];
+
+ for (const claim of groupClaims) {
+ if (profile[claim]) {
+ return Array.isArray(profile[claim]) ? profile[claim] : [profile[claim]];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Extract roles from SAML assertion
+ */
+ extractRoles(profile) {
+ const roleClaims = [
+ 'http://schemas.microsoft.com/ws/2008/06/identity/claims/role',
+ 'http://schemas.xmlsoap.org/claims/Role',
+ 'roles'
+ ];
+
+ for (const claim of roleClaims) {
+ if (profile[claim]) {
+ return Array.isArray(profile[claim]) ? profile[claim] : [profile[claim]];
+ }
+ }
+
+ return [];
+ }
+
+ /**
+ * Initiate SAML authentication
+ */
+ async authenticate(req, res, next) {
+ const tenantId = req.tenant.id;
+ const strategyName = `saml-${tenantId}`;
+
+ // Check if strategy is configured for this tenant
+ if (!this.strategies.has(tenantId)) {
+ const config = req.tenant.authConfig.config;
+ this.configureSAMLStrategy(tenantId, config);
+ }
+
+ // Use passport to authenticate
+ passport.authenticate(strategyName, {
+ additionalParams: {
+ RelayState: req.query.returnUrl || '/'
+ }
+ })(req, res, next);
+ }
+
+ /**
+ * Handle SAML callback
+ */
+ async handleCallback(req, res, next) {
+ const tenantId = req.params.tenantId;
+ const strategyName = `saml-${tenantId}`;
+
+ passport.authenticate(strategyName, async (err, authResult) => {
+ if (err) {
+ console.error('SAML authentication error:', err);
+ return res.redirect(`/login?error=auth_failed&tenant=${tenantId}`);
+ }
+
+ if (!authResult) {
+ return res.redirect(`/login?error=auth_cancelled&tenant=${tenantId}`);
+ }
+
+ try {
+ const { MultiTenantAuth } = require('./multi-tenant-auth');
+ const multiAuth = new MultiTenantAuth();
+
+ // Create or update user
+ const user = await multiAuth.createOrUpdateExternalUser(
+ tenantId,
+ authResult.externalUser,
+ req.tenant.authConfig
+ );
+
+ // Generate JWT token
+ const token = multiAuth.generateJWTToken(user, tenantId);
+
+ // Redirect to application with token
+ const returnUrl = req.body.RelayState || '/';
+ res.redirect(`${returnUrl}?token=${token}&tenant=${tenantId}`);
+
+ } catch (error) {
+ console.error('SAML user creation error:', error);
+ return res.redirect(`/login?error=user_creation_failed&tenant=${tenantId}`);
+ }
+ })(req, res, next);
+ }
+
+ /**
+ * Generate SAML metadata for SP configuration
+ */
+ generateMetadata(tenantId, config) {
+ const metadata = `
+
+
+ urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
+
+
+
+`;
+
+ return metadata;
+ }
+}
+
+module.exports = SAMLAuth;
diff --git a/server/migrations/20250912000001-add-multi-tenant-support.js b/server/migrations/20250912000001-add-multi-tenant-support.js
new file mode 100644
index 0000000..f98d359
--- /dev/null
+++ b/server/migrations/20250912000001-add-multi-tenant-support.js
@@ -0,0 +1,267 @@
+/**
+ * Migration: Add Multi-Tenant Support
+ * Adds tenant table and updates user table for multi-tenancy
+ */
+
+'use strict';
+
+module.exports = {
+ async up(queryInterface, Sequelize) {
+ // Create tenants table
+ await queryInterface.createTable('tenants', {
+ id: {
+ type: Sequelize.UUID,
+ defaultValue: Sequelize.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ comment: 'Human-readable tenant name'
+ },
+ slug: {
+ type: Sequelize.STRING,
+ allowNull: false,
+ unique: true,
+ comment: 'URL-safe tenant identifier'
+ },
+ domain: {
+ type: Sequelize.STRING,
+ allowNull: true,
+ comment: 'Custom domain for this tenant'
+ },
+ subscription_type: {
+ type: Sequelize.ENUM('free', 'basic', 'premium', 'enterprise'),
+ defaultValue: 'basic',
+ comment: 'Subscription tier'
+ },
+ is_active: {
+ type: Sequelize.BOOLEAN,
+ defaultValue: true,
+ comment: 'Whether tenant is active'
+ },
+ auth_provider: {
+ type: Sequelize.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'),
+ defaultValue: 'local',
+ comment: 'Primary authentication provider'
+ },
+ auth_config: {
+ type: Sequelize.JSONB,
+ allowNull: true,
+ comment: 'Authentication provider configuration'
+ },
+ user_mapping: {
+ type: Sequelize.JSONB,
+ allowNull: true,
+ comment: 'User attribute mapping from external provider'
+ },
+ role_mapping: {
+ type: Sequelize.JSONB,
+ allowNull: true,
+ comment: 'Role mapping from external provider to internal roles'
+ },
+ branding: {
+ type: Sequelize.JSONB,
+ allowNull: true,
+ comment: 'Tenant-specific branding'
+ },
+ features: {
+ type: Sequelize.JSONB,
+ defaultValue: {
+ max_devices: 10,
+ max_users: 5,
+ api_rate_limit: 1000,
+ data_retention_days: 90,
+ features: ['basic_detection', 'alerts', 'dashboard']
+ },
+ comment: 'Tenant feature limits and enabled features'
+ },
+ admin_email: {
+ type: Sequelize.STRING,
+ allowNull: true,
+ comment: 'Primary admin email for this tenant'
+ },
+ admin_phone: {
+ type: Sequelize.STRING,
+ allowNull: true,
+ comment: 'Primary admin phone for this tenant'
+ },
+ billing_email: {
+ type: Sequelize.STRING,
+ allowNull: true
+ },
+ payment_method_id: {
+ type: Sequelize.STRING,
+ allowNull: true,
+ comment: 'Payment provider customer ID'
+ },
+ metadata: {
+ type: Sequelize.JSONB,
+ allowNull: true,
+ comment: 'Additional tenant metadata'
+ },
+ created_at: {
+ type: Sequelize.DATE,
+ defaultValue: Sequelize.NOW
+ },
+ updated_at: {
+ type: Sequelize.DATE,
+ defaultValue: Sequelize.NOW
+ }
+ });
+
+ // Add indexes to tenants table
+ await queryInterface.addIndex('tenants', ['slug'], { unique: true });
+ await queryInterface.addIndex('tenants', ['domain'], {
+ unique: true,
+ where: { domain: { [Sequelize.Op.ne]: null } }
+ });
+ await queryInterface.addIndex('tenants', ['is_active']);
+ await queryInterface.addIndex('tenants', ['auth_provider']);
+
+ // Add tenant-related columns to users table
+ await queryInterface.addColumn('users', 'tenant_id', {
+ type: Sequelize.UUID,
+ allowNull: true,
+ references: {
+ model: 'tenants',
+ key: 'id'
+ },
+ comment: 'Tenant this user belongs to'
+ });
+
+ await queryInterface.addColumn('users', 'external_provider', {
+ type: Sequelize.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'),
+ defaultValue: 'local',
+ comment: 'Authentication provider used for this user'
+ });
+
+ await queryInterface.addColumn('users', 'external_id', {
+ type: Sequelize.STRING,
+ allowNull: true,
+ comment: 'User ID from external authentication provider'
+ });
+
+ // Add indexes to users table
+ await queryInterface.addIndex('users', ['tenant_id']);
+ await queryInterface.addIndex('users', ['external_provider']);
+ await queryInterface.addIndex('users', ['external_id', 'tenant_id'], {
+ unique: true,
+ name: 'users_external_id_tenant_unique',
+ where: { external_id: { [Sequelize.Op.ne]: null } }
+ });
+
+ // Create default tenant for backward compatibility
+ const defaultTenantId = await queryInterface.bulkInsert('tenants', [{
+ id: Sequelize.literal('gen_random_uuid()'),
+ name: 'Default Organization',
+ slug: 'default',
+ subscription_type: 'enterprise',
+ is_active: true,
+ auth_provider: 'local',
+ features: JSON.stringify({
+ max_devices: -1,
+ max_users: -1,
+ api_rate_limit: 50000,
+ data_retention_days: -1,
+ features: ['all']
+ }),
+ created_at: new Date(),
+ updated_at: new Date()
+ }], { returning: true });
+
+ // Associate existing users with default tenant
+ await queryInterface.sequelize.query(`
+ UPDATE users
+ SET tenant_id = (SELECT id FROM tenants WHERE slug = 'default')
+ WHERE tenant_id IS NULL
+ `);
+
+ // Add tenant_id to devices table if it exists
+ try {
+ await queryInterface.addColumn('devices', 'tenant_id', {
+ type: Sequelize.UUID,
+ allowNull: true,
+ references: {
+ model: 'tenants',
+ key: 'id'
+ },
+ comment: 'Tenant this device belongs to'
+ });
+
+ await queryInterface.addIndex('devices', ['tenant_id']);
+
+ // Associate existing devices with default tenant
+ await queryInterface.sequelize.query(`
+ UPDATE devices
+ SET tenant_id = (SELECT id FROM tenants WHERE slug = 'default')
+ WHERE tenant_id IS NULL
+ `);
+ } catch (error) {
+ console.log('Devices table not found or already has tenant_id column');
+ }
+
+ // Add tenant_id to alert_rules table if it exists
+ try {
+ await queryInterface.addColumn('alert_rules', 'tenant_id', {
+ type: Sequelize.UUID,
+ allowNull: true,
+ references: {
+ model: 'tenants',
+ key: 'id'
+ },
+ comment: 'Tenant this alert rule belongs to'
+ });
+
+ await queryInterface.addIndex('alert_rules', ['tenant_id']);
+
+ // Associate existing alert rules with default tenant
+ await queryInterface.sequelize.query(`
+ UPDATE alert_rules
+ SET tenant_id = (SELECT id FROM tenants WHERE slug = 'default')
+ WHERE tenant_id IS NULL
+ `);
+ } catch (error) {
+ console.log('Alert_rules table not found or already has tenant_id column');
+ }
+
+ console.log('โ
Multi-tenant support added successfully');
+ console.log('โ
Default tenant created for backward compatibility');
+ console.log('โ
Existing data associated with default tenant');
+ },
+
+ async down(queryInterface, Sequelize) {
+ // Remove indexes from users table
+ await queryInterface.removeIndex('users', ['tenant_id']);
+ await queryInterface.removeIndex('users', ['external_provider']);
+ await queryInterface.removeIndex('users', 'users_external_id_tenant_unique');
+
+ // Remove columns from users table
+ await queryInterface.removeColumn('users', 'tenant_id');
+ await queryInterface.removeColumn('users', 'external_provider');
+ await queryInterface.removeColumn('users', 'external_id');
+
+ // Remove tenant_id from other tables
+ try {
+ await queryInterface.removeColumn('devices', 'tenant_id');
+ } catch (error) {
+ console.log('Devices table tenant_id column not found');
+ }
+
+ try {
+ await queryInterface.removeColumn('alert_rules', 'tenant_id');
+ } catch (error) {
+ console.log('Alert_rules table tenant_id column not found');
+ }
+
+ // Drop ENUMs
+ await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_users_external_provider"');
+ await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_tenants_auth_provider"');
+ await queryInterface.sequelize.query('DROP TYPE IF EXISTS "enum_tenants_subscription_type"');
+
+ // Drop tenants table
+ await queryInterface.dropTable('tenants');
+
+ console.log('โ
Multi-tenant support removed');
+ }
+};
diff --git a/server/models/Tenant.js b/server/models/Tenant.js
new file mode 100644
index 0000000..b2b5e65
--- /dev/null
+++ b/server/models/Tenant.js
@@ -0,0 +1,213 @@
+/**
+ * Tenant Model for Multi-Tenant Support
+ * Stores tenant-specific configuration including authentication providers
+ */
+
+const { DataTypes } = require('sequelize');
+
+module.exports = (sequelize) => {
+ const Tenant = sequelize.define('Tenant', {
+ id: {
+ type: DataTypes.UUID,
+ defaultValue: DataTypes.UUIDV4,
+ primaryKey: true
+ },
+ name: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ comment: 'Human-readable tenant name'
+ },
+ slug: {
+ type: DataTypes.STRING,
+ allowNull: false,
+ unique: true,
+ validate: {
+ is: /^[a-z0-9-]+$/i // Alphanumeric and hyphens only
+ },
+ comment: 'URL-safe tenant identifier (subdomain/path)'
+ },
+ domain: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ comment: 'Custom domain for this tenant'
+ },
+ subscription_type: {
+ type: DataTypes.ENUM('free', 'basic', 'premium', 'enterprise'),
+ defaultValue: 'basic',
+ comment: 'Subscription tier'
+ },
+ is_active: {
+ type: DataTypes.BOOLEAN,
+ defaultValue: true,
+ comment: 'Whether tenant is active'
+ },
+
+ // Authentication Configuration
+ auth_provider: {
+ type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'),
+ defaultValue: 'local',
+ comment: 'Primary authentication provider'
+ },
+ auth_config: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ comment: 'Authentication provider configuration (encrypted)'
+ },
+ user_mapping: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ comment: 'User attribute mapping from external provider'
+ },
+ role_mapping: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ comment: 'Role mapping from external provider to internal roles'
+ },
+
+ // Tenant Customization
+ branding: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ comment: 'Tenant-specific branding (logo, colors, etc.)'
+ },
+ features: {
+ type: DataTypes.JSONB,
+ defaultValue: {
+ max_devices: 10,
+ max_users: 5,
+ api_rate_limit: 1000,
+ data_retention_days: 90,
+ features: ['basic_detection', 'alerts', 'dashboard']
+ },
+ comment: 'Tenant feature limits and enabled features'
+ },
+
+ // Contact Information
+ admin_email: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ validate: {
+ isEmail: true
+ },
+ comment: 'Primary admin email for this tenant'
+ },
+ admin_phone: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ comment: 'Primary admin phone for this tenant'
+ },
+
+ // Billing Information
+ billing_email: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ validate: {
+ isEmail: true
+ }
+ },
+ payment_method_id: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ comment: 'Stripe/payment provider customer ID'
+ },
+
+ // Metadata
+ metadata: {
+ type: DataTypes.JSONB,
+ allowNull: true,
+ comment: 'Additional tenant metadata'
+ },
+
+ created_at: {
+ type: DataTypes.DATE,
+ defaultValue: DataTypes.NOW
+ },
+ updated_at: {
+ type: DataTypes.DATE,
+ defaultValue: DataTypes.NOW
+ }
+ }, {
+ tableName: 'tenants',
+ timestamps: true,
+ createdAt: 'created_at',
+ updatedAt: 'updated_at',
+ indexes: [
+ {
+ fields: ['slug'],
+ unique: true
+ },
+ {
+ fields: ['domain'],
+ unique: true,
+ where: { domain: { [DataTypes.Op.ne]: null } }
+ },
+ {
+ fields: ['is_active']
+ },
+ {
+ fields: ['auth_provider']
+ }
+ ],
+ hooks: {
+ beforeSave: (tenant) => {
+ // Encrypt sensitive auth configuration
+ if (tenant.auth_config && typeof tenant.auth_config === 'object') {
+ // In production, encrypt sensitive fields like client_secret, private_key, etc.
+ const sensitiveFields = ['client_secret', 'private_key', 'bind_password', 'admin_password'];
+ sensitiveFields.forEach(field => {
+ if (tenant.auth_config[field]) {
+ // Simple base64 encoding for demo - use proper encryption in production
+ tenant.auth_config[field] = Buffer.from(tenant.auth_config[field]).toString('base64');
+ }
+ });
+ }
+ },
+ afterFind: (tenants) => {
+ // Decrypt auth configuration after retrieval
+ const processOne = (tenant) => {
+ if (tenant.auth_config && typeof tenant.auth_config === 'object') {
+ const sensitiveFields = ['client_secret', 'private_key', 'bind_password', 'admin_password'];
+ sensitiveFields.forEach(field => {
+ if (tenant.auth_config[field]) {
+ try {
+ tenant.auth_config[field] = Buffer.from(tenant.auth_config[field], 'base64').toString();
+ } catch (e) {
+ // Field might not be encrypted, leave as-is
+ }
+ }
+ });
+ }
+ };
+
+ if (Array.isArray(tenants)) {
+ tenants.forEach(processOne);
+ } else if (tenants) {
+ processOne(tenants);
+ }
+ }
+ }
+ });
+
+ // Associations
+ Tenant.associate = (models) => {
+ // A tenant has many users
+ Tenant.hasMany(models.User, {
+ foreignKey: 'tenant_id',
+ as: 'users'
+ });
+
+ // A tenant has many devices
+ Tenant.hasMany(models.Device, {
+ foreignKey: 'tenant_id',
+ as: 'devices'
+ });
+
+ // A tenant has many alert rules
+ Tenant.hasMany(models.AlertRule, {
+ foreignKey: 'tenant_id',
+ as: 'alertRules'
+ });
+ };
+
+ return Tenant;
+};
diff --git a/server/models/User.js b/server/models/User.js
index dc4107b..89c353d 100644
--- a/server/models/User.js
+++ b/server/models/User.js
@@ -68,6 +68,25 @@ module.exports = (sequelize) => {
defaultValue: 'UTC',
comment: 'User timezone for alert scheduling'
},
+ tenant_id: {
+ type: DataTypes.UUID,
+ allowNull: true,
+ references: {
+ model: 'tenants',
+ key: 'id'
+ },
+ comment: 'Tenant this user belongs to (null for default tenant)'
+ },
+ external_provider: {
+ type: DataTypes.ENUM('local', 'saml', 'oauth', 'ldap', 'custom_sso'),
+ defaultValue: 'local',
+ comment: 'Authentication provider used for this user'
+ },
+ external_id: {
+ type: DataTypes.STRING,
+ allowNull: true,
+ comment: 'User ID from external authentication provider'
+ },
created_at: {
type: DataTypes.DATE,
defaultValue: DataTypes.NOW
@@ -90,9 +109,30 @@ module.exports = (sequelize) => {
},
{
fields: ['phone_number']
+ },
+ {
+ fields: ['tenant_id']
+ },
+ {
+ fields: ['external_provider']
+ },
+ {
+ fields: ['external_id', 'tenant_id'],
+ unique: true,
+ name: 'users_external_id_tenant_unique',
+ where: { external_id: { [DataTypes.Op.ne]: null } }
}
]
});
+ // Associations
+ User.associate = (models) => {
+ // User belongs to a tenant
+ User.belongsTo(models.Tenant, {
+ foreignKey: 'tenant_id',
+ as: 'tenant'
+ });
+ };
+
return User;
};
diff --git a/server/package.json b/server/package.json
index ec64f9f..6034f52 100644
--- a/server/package.json
+++ b/server/package.json
@@ -25,7 +25,13 @@
"bcryptjs": "^2.4.3",
"jsonwebtoken": "^9.0.1",
"express-rate-limit": "^6.8.1",
- "compression": "^1.7.4"
+ "compression": "^1.7.4",
+ "passport": "^0.6.0",
+ "passport-saml": "^3.2.4",
+ "passport-oauth2": "^1.7.0",
+ "passport-openidconnect": "^0.1.1",
+ "ldapjs": "^3.0.7",
+ "express-session": "^1.17.3"
},
"devDependencies": {
"nodemon": "^3.0.1",
diff --git a/server/routes/auth.js b/server/routes/auth.js
new file mode 100644
index 0000000..973256c
--- /dev/null
+++ b/server/routes/auth.js
@@ -0,0 +1,313 @@
+/**
+ * Multi-Tenant Authentication Routes
+ * Handles authentication for different providers and tenants
+ */
+
+const express = require('express');
+const router = express.Router();
+const passport = require('passport');
+const session = require('express-session');
+const { Tenant } = require('../models');
+const MultiTenantAuth = require('../middleware/multi-tenant-auth');
+const SAMLAuth = require('../middleware/saml-auth');
+const OAuthAuth = require('../middleware/oauth-auth');
+const LDAPAuth = require('../middleware/ldap-auth');
+
+// Initialize multi-tenant auth
+const multiAuth = new MultiTenantAuth();
+
+// Session middleware for OAuth state management
+router.use(session({
+ secret: process.env.SESSION_SECRET || 'your-session-secret',
+ resave: false,
+ saveUninitialized: false,
+ cookie: { maxAge: 10 * 60 * 1000 } // 10 minutes
+}));
+
+// Initialize passport
+router.use(passport.initialize());
+router.use(passport.session());
+
+passport.serializeUser((user, done) => done(null, user));
+passport.deserializeUser((obj, done) => done(null, obj));
+
+/**
+ * GET /auth/config/:tenantId
+ * Get authentication configuration for a tenant
+ */
+router.get('/config/:tenantId', async (req, res) => {
+ try {
+ const { tenantId } = req.params;
+
+ const tenant = await Tenant.findOne({ where: { slug: tenantId } });
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Return public auth configuration (no secrets)
+ const publicConfig = {
+ provider: tenant.auth_provider,
+ enabled: tenant.is_active,
+ features: {
+ local_login: tenant.auth_provider === 'local',
+ sso_login: ['saml', 'oauth', 'ldap'].includes(tenant.auth_provider),
+ registration: tenant.auth_provider === 'local'
+ }
+ };
+
+ // Add provider-specific public config
+ if (tenant.auth_provider === 'saml') {
+ publicConfig.saml = {
+ login_url: `/auth/saml/${tenantId}/login`,
+ metadata_url: `/auth/saml/${tenantId}/metadata`
+ };
+ } else if (tenant.auth_provider === 'oauth') {
+ publicConfig.oauth = {
+ login_url: `/auth/oauth/${tenantId}/login`
+ };
+ }
+
+ res.json({
+ success: true,
+ data: publicConfig
+ });
+
+ } catch (error) {
+ console.error('Error fetching auth config:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to fetch authentication configuration'
+ });
+ }
+});
+
+/**
+ * POST /auth/login
+ * Universal login endpoint that routes to appropriate provider
+ */
+router.post('/login', async (req, res, next) => {
+ try {
+ // Determine tenant
+ const tenantId = await multiAuth.determineTenant(req);
+ const authConfig = await multiAuth.getTenantAuthConfig(tenantId);
+
+ req.tenant = { id: tenantId, authConfig };
+
+ // Route based on authentication provider
+ switch (authConfig.type) {
+ case 'local':
+ return require('../routes/user').loginLocal(req, res, next);
+
+ case 'ldap':
+ const ldapAuth = new LDAPAuth();
+ return ldapAuth.authenticate(req, res, next);
+
+ case 'saml':
+ case 'oauth':
+ return res.status(400).json({
+ success: false,
+ message: `Please use SSO login for ${authConfig.type} authentication`,
+ redirect_url: `/auth/${authConfig.type}/${tenantId}/login`
+ });
+
+ default:
+ return res.status(400).json({
+ success: false,
+ message: 'Authentication provider not configured'
+ });
+ }
+
+ } catch (error) {
+ console.error('Login error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Login failed'
+ });
+ }
+});
+
+/**
+ * SAML Authentication Routes
+ */
+
+// GET /auth/saml/:tenantId/login - Initiate SAML login
+router.get('/saml/:tenantId/login', async (req, res, next) => {
+ try {
+ const tenantId = req.params.tenantId;
+ req.tenant = {
+ id: tenantId,
+ authConfig: await multiAuth.getTenantAuthConfig(tenantId)
+ };
+
+ const samlAuth = new SAMLAuth();
+ return samlAuth.authenticate(req, res, next);
+
+ } catch (error) {
+ console.error('SAML login error:', error);
+ res.redirect(`/login?error=saml_error&tenant=${req.params.tenantId}`);
+ }
+});
+
+// POST /auth/saml/:tenantId/callback - SAML callback
+router.post('/saml/:tenantId/callback', async (req, res, next) => {
+ const samlAuth = new SAMLAuth();
+ return samlAuth.handleCallback(req, res, next);
+});
+
+// GET /auth/saml/:tenantId/metadata - SAML metadata
+router.get('/saml/:tenantId/metadata', async (req, res) => {
+ try {
+ const { tenantId } = req.params;
+ const authConfig = await multiAuth.getTenantAuthConfig(tenantId);
+
+ if (authConfig.type !== 'saml') {
+ return res.status(404).json({ message: 'SAML not configured for this tenant' });
+ }
+
+ const samlAuth = new SAMLAuth();
+ const metadata = samlAuth.generateMetadata(tenantId, authConfig.config);
+
+ res.set('Content-Type', 'application/xml');
+ res.send(metadata);
+
+ } catch (error) {
+ console.error('SAML metadata error:', error);
+ res.status(500).json({ message: 'Failed to generate SAML metadata' });
+ }
+});
+
+/**
+ * OAuth Authentication Routes
+ */
+
+// GET /auth/oauth/:tenantId/login - Initiate OAuth login
+router.get('/oauth/:tenantId/login', async (req, res, next) => {
+ try {
+ const tenantId = req.params.tenantId;
+ req.tenant = {
+ id: tenantId,
+ authConfig: await multiAuth.getTenantAuthConfig(tenantId)
+ };
+
+ const oauthAuth = new OAuthAuth();
+ return oauthAuth.authenticate(req, res, next);
+
+ } catch (error) {
+ console.error('OAuth login error:', error);
+ res.redirect(`/login?error=oauth_error&tenant=${req.params.tenantId}`);
+ }
+});
+
+// GET /auth/oauth/:tenantId/callback - OAuth callback
+router.get('/oauth/:tenantId/callback', async (req, res, next) => {
+ const oauthAuth = new OAuthAuth();
+ return oauthAuth.handleCallback(req, res, next);
+});
+
+/**
+ * Logout endpoint for all providers
+ */
+router.post('/logout', async (req, res) => {
+ try {
+ const tenantId = await multiAuth.determineTenant(req);
+ const authConfig = await multiAuth.getTenantAuthConfig(tenantId);
+
+ // Clear local session
+ req.logout((err) => {
+ if (err) console.error('Logout error:', err);
+ });
+
+ // Provider-specific logout
+ if (authConfig.type === 'saml' && authConfig.config.logout_url) {
+ return res.json({
+ success: true,
+ logout_url: authConfig.config.logout_url,
+ message: 'Please complete logout with your identity provider'
+ });
+ }
+
+ res.json({
+ success: true,
+ message: 'Logged out successfully'
+ });
+
+ } catch (error) {
+ console.error('Logout error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Logout failed'
+ });
+ }
+});
+
+/**
+ * Test authentication configuration
+ */
+router.post('/test/:tenantId', async (req, res) => {
+ try {
+ const { tenantId } = req.params;
+ const authConfig = await multiAuth.getTenantAuthConfig(tenantId);
+
+ let testResult = { success: false, message: 'Unknown provider' };
+
+ switch (authConfig.type) {
+ case 'ldap':
+ const ldapAuth = new LDAPAuth();
+ try {
+ await ldapAuth.testConnection(authConfig.config);
+ testResult = { success: true, message: 'LDAP connection successful' };
+ } catch (error) {
+ testResult = { success: false, message: error.message };
+ }
+ break;
+
+ case 'local':
+ testResult = { success: true, message: 'Local authentication ready' };
+ break;
+
+ case 'saml':
+ // Test SAML configuration validity
+ const requiredSamlFields = ['sso_url', 'certificate', 'issuer'];
+ const missingSamlFields = requiredSamlFields.filter(field => !authConfig.config[field]);
+
+ if (missingSamlFields.length > 0) {
+ testResult = {
+ success: false,
+ message: `Missing SAML configuration: ${missingSamlFields.join(', ')}`
+ };
+ } else {
+ testResult = { success: true, message: 'SAML configuration valid' };
+ }
+ break;
+
+ case 'oauth':
+ // Test OAuth configuration validity
+ const requiredOAuthFields = ['client_id', 'client_secret', 'authorization_url', 'token_url'];
+ const missingOAuthFields = requiredOAuthFields.filter(field => !authConfig.config[field]);
+
+ if (missingOAuthFields.length > 0) {
+ testResult = {
+ success: false,
+ message: `Missing OAuth configuration: ${missingOAuthFields.join(', ')}`
+ };
+ } else {
+ testResult = { success: true, message: 'OAuth configuration valid' };
+ }
+ break;
+ }
+
+ res.json(testResult);
+
+ } catch (error) {
+ console.error('Auth test error:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Authentication test failed'
+ });
+ }
+});
+
+module.exports = router;
diff --git a/server/routes/tenants.js b/server/routes/tenants.js
new file mode 100644
index 0000000..0ddce8f
--- /dev/null
+++ b/server/routes/tenants.js
@@ -0,0 +1,381 @@
+/**
+ * Tenant Management Routes
+ * Admin interface for managing tenants and their authentication configurations
+ */
+
+const express = require('express');
+const router = express.Router();
+const Joi = require('joi');
+const { Tenant, User } = require('../models');
+const { validateRequest } = require('../middleware/validation');
+const { authenticateToken, requireRole } = require('../middleware/auth');
+
+// Validation schemas
+const tenantSchema = Joi.object({
+ name: Joi.string().required(),
+ slug: Joi.string().pattern(/^[a-z0-9-]+$/).required(),
+ domain: Joi.string().optional(),
+ subscription_type: Joi.string().valid('free', 'basic', 'premium', 'enterprise').default('basic'),
+ auth_provider: Joi.string().valid('local', 'saml', 'oauth', 'ldap', 'custom_sso').default('local'),
+ auth_config: Joi.object().optional(),
+ user_mapping: Joi.object().optional(),
+ role_mapping: Joi.object().optional(),
+ branding: Joi.object().optional(),
+ features: Joi.object().optional(),
+ admin_email: Joi.string().email().optional(),
+ admin_phone: Joi.string().optional(),
+ billing_email: Joi.string().email().optional()
+});
+
+const authConfigSchema = Joi.object({
+ // SAML Configuration
+ sso_url: Joi.string().uri().when('auth_provider', { is: 'saml', then: Joi.required() }),
+ certificate: Joi.string().when('auth_provider', { is: 'saml', then: Joi.required() }),
+ issuer: Joi.string().when('auth_provider', { is: 'saml', then: Joi.required() }),
+ logout_url: Joi.string().uri().optional(),
+
+ // OAuth Configuration
+ client_id: Joi.string().when('auth_provider', { is: 'oauth', then: Joi.required() }),
+ client_secret: Joi.string().when('auth_provider', { is: 'oauth', then: Joi.required() }),
+ authorization_url: Joi.string().uri().when('auth_provider', { is: 'oauth', then: Joi.required() }),
+ token_url: Joi.string().uri().when('auth_provider', { is: 'oauth', then: Joi.required() }),
+ userinfo_url: Joi.string().uri().optional(),
+ scopes: Joi.array().items(Joi.string()).optional(),
+
+ // LDAP Configuration
+ url: Joi.string().when('auth_provider', { is: 'ldap', then: Joi.required() }),
+ base_dn: Joi.string().when('auth_provider', { is: 'ldap', then: Joi.required() }),
+ bind_dn: Joi.string().optional(),
+ bind_password: Joi.string().optional(),
+ user_search_filter: Joi.string().optional(),
+ domain: Joi.string().optional()
+});
+
+/**
+ * GET /api/tenants - List all tenants (super admin only)
+ */
+router.get('/', authenticateToken, requireRole(['admin']), async (req, res) => {
+ try {
+ const { limit = 50, offset = 0, search, auth_provider } = req.query;
+
+ const whereClause = {};
+ if (search) {
+ whereClause[Op.or] = [
+ { name: { [Op.iLike]: `%${search}%` } },
+ { slug: { [Op.iLike]: `%${search}%` } },
+ { domain: { [Op.iLike]: `%${search}%` } }
+ ];
+ }
+ if (auth_provider) {
+ whereClause.auth_provider = auth_provider;
+ }
+
+ const tenants = await Tenant.findAndCountAll({
+ where: whereClause,
+ attributes: { exclude: ['auth_config'] }, // Don't expose auth secrets
+ include: [{
+ model: User,
+ as: 'users',
+ attributes: ['id', 'username', 'email', 'role'],
+ limit: 5
+ }],
+ limit: Math.min(parseInt(limit), 100),
+ offset: parseInt(offset),
+ order: [['created_at', 'DESC']]
+ });
+
+ res.json({
+ success: true,
+ data: tenants.rows,
+ pagination: {
+ total: tenants.count,
+ limit: parseInt(limit),
+ offset: parseInt(offset),
+ pages: Math.ceil(tenants.count / parseInt(limit))
+ }
+ });
+
+ } catch (error) {
+ console.error('Error fetching tenants:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to fetch tenants'
+ });
+ }
+});
+
+/**
+ * POST /api/tenants - Create new tenant
+ */
+router.post('/', authenticateToken, requireRole(['admin']), validateRequest(tenantSchema), async (req, res) => {
+ try {
+ const tenantData = req.body;
+
+ // Check if slug is unique
+ const existingTenant = await Tenant.findOne({ where: { slug: tenantData.slug } });
+ if (existingTenant) {
+ return res.status(409).json({
+ success: false,
+ message: 'Tenant slug already exists'
+ });
+ }
+
+ // Set default features based on subscription type
+ if (!tenantData.features) {
+ tenantData.features = getDefaultFeatures(tenantData.subscription_type);
+ }
+
+ const tenant = await Tenant.create(tenantData);
+
+ res.status(201).json({
+ success: true,
+ data: tenant,
+ message: 'Tenant created successfully'
+ });
+
+ } catch (error) {
+ console.error('Error creating tenant:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to create tenant'
+ });
+ }
+});
+
+/**
+ * GET /api/tenants/:id - Get tenant details
+ */
+router.get('/:id', authenticateToken, requireRole(['admin']), async (req, res) => {
+ try {
+ const tenant = await Tenant.findByPk(req.params.id, {
+ include: [{
+ model: User,
+ as: 'users',
+ attributes: ['id', 'username', 'email', 'role', 'last_login', 'created_at']
+ }]
+ });
+
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Mask sensitive auth configuration
+ const tenantData = tenant.toJSON();
+ if (tenantData.auth_config) {
+ tenantData.auth_config = maskSensitiveConfig(tenantData.auth_config);
+ }
+
+ res.json({
+ success: true,
+ data: tenantData
+ });
+
+ } catch (error) {
+ console.error('Error fetching tenant:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to fetch tenant'
+ });
+ }
+});
+
+/**
+ * PUT /api/tenants/:id - Update tenant
+ */
+router.put('/:id', authenticateToken, requireRole(['admin']), async (req, res) => {
+ try {
+ const tenant = await Tenant.findByPk(req.params.id);
+
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ await tenant.update(req.body);
+
+ res.json({
+ success: true,
+ data: tenant,
+ message: 'Tenant updated successfully'
+ });
+
+ } catch (error) {
+ console.error('Error updating tenant:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to update tenant'
+ });
+ }
+});
+
+/**
+ * PUT /api/tenants/:id/auth-config - Update tenant authentication configuration
+ */
+router.put('/:id/auth-config', authenticateToken, requireRole(['admin']), async (req, res) => {
+ try {
+ const tenant = await Tenant.findByPk(req.params.id);
+
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ const { auth_provider, auth_config, user_mapping, role_mapping } = req.body;
+
+ // Validate auth configuration based on provider
+ const validationSchema = authConfigSchema.fork('auth_provider', (schema) =>
+ schema.default(auth_provider)
+ );
+
+ const { error } = validationSchema.validate({ auth_provider, ...auth_config });
+ if (error) {
+ return res.status(400).json({
+ success: false,
+ message: 'Invalid authentication configuration',
+ details: error.details
+ });
+ }
+
+ await tenant.update({
+ auth_provider,
+ auth_config,
+ user_mapping,
+ role_mapping
+ });
+
+ res.json({
+ success: true,
+ message: 'Authentication configuration updated successfully'
+ });
+
+ } catch (error) {
+ console.error('Error updating auth config:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to update authentication configuration'
+ });
+ }
+});
+
+/**
+ * POST /api/tenants/:id/test-auth - Test authentication configuration
+ */
+router.post('/:id/test-auth', authenticateToken, requireRole(['admin']), async (req, res) => {
+ try {
+ const tenant = await Tenant.findByPk(req.params.id);
+
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Use the auth test endpoint
+ const authTestRoute = require('./auth');
+ req.params.tenantId = tenant.slug;
+
+ return authTestRoute.testAuth(req, res);
+
+ } catch (error) {
+ console.error('Error testing auth config:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to test authentication configuration'
+ });
+ }
+});
+
+/**
+ * DELETE /api/tenants/:id - Delete tenant (soft delete)
+ */
+router.delete('/:id', authenticateToken, requireRole(['admin']), async (req, res) => {
+ try {
+ const tenant = await Tenant.findByPk(req.params.id);
+
+ if (!tenant) {
+ return res.status(404).json({
+ success: false,
+ message: 'Tenant not found'
+ });
+ }
+
+ // Soft delete by setting is_active to false
+ await tenant.update({ is_active: false });
+
+ res.json({
+ success: true,
+ message: 'Tenant deactivated successfully'
+ });
+
+ } catch (error) {
+ console.error('Error deleting tenant:', error);
+ res.status(500).json({
+ success: false,
+ message: 'Failed to delete tenant'
+ });
+ }
+});
+
+/**
+ * Helper function to get default features based on subscription type
+ */
+function getDefaultFeatures(subscriptionType) {
+ const featureMap = {
+ free: {
+ max_devices: 2,
+ max_users: 1,
+ api_rate_limit: 100,
+ data_retention_days: 7,
+ features: ['basic_detection']
+ },
+ basic: {
+ max_devices: 10,
+ max_users: 5,
+ api_rate_limit: 1000,
+ data_retention_days: 90,
+ features: ['basic_detection', 'alerts', 'dashboard']
+ },
+ premium: {
+ max_devices: 50,
+ max_users: 20,
+ api_rate_limit: 5000,
+ data_retention_days: 365,
+ features: ['basic_detection', 'alerts', 'dashboard', 'advanced_analytics', 'api_access']
+ },
+ enterprise: {
+ max_devices: -1, // Unlimited
+ max_users: -1, // Unlimited
+ api_rate_limit: 50000,
+ data_retention_days: -1, // Unlimited
+ features: ['all']
+ }
+ };
+
+ return featureMap[subscriptionType] || featureMap.basic;
+}
+
+/**
+ * Helper function to mask sensitive configuration data
+ */
+function maskSensitiveConfig(config) {
+ const maskedConfig = { ...config };
+ const sensitiveFields = ['client_secret', 'private_key', 'bind_password', 'admin_password'];
+
+ sensitiveFields.forEach(field => {
+ if (maskedConfig[field]) {
+ maskedConfig[field] = '****';
+ }
+ });
+
+ return maskedConfig;
+}
+
+module.exports = router;