/** * 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;