326 lines
8.7 KiB
JavaScript
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;
|