Fix jwt-token
This commit is contained in:
325
server/middleware/ldap-auth.js
Normal file
325
server/middleware/ldap-auth.js
Normal file
@@ -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;
|
||||
Reference in New Issue
Block a user