Files
drone-detector/server/middleware/ldap-auth.js
2025-09-12 12:11:14 +02:00

326 lines
8.7 KiB
JavaScript

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