Diverse anpassungen

This commit is contained in:
2026-02-03 23:25:37 +01:00
parent a3efbb43ae
commit d6e985998a
7 changed files with 332 additions and 94 deletions

View File

@@ -125,20 +125,35 @@ class LDAPService {
* LDAP-Filter unterstützen UTF-8 direkt nach RFC 4515.
* Nur die speziellen LDAP-Filter-Zeichen werden escaped.
*/
static escapeLDAPFilter(value) {
static escapeLDAPFilter(value, debugLabel = '') {
if (!value) return '';
// Stelle sicher, dass der Wert als String behandelt wird
const str = String(value);
// Debug-Logging
if (debugLabel) {
console.log(`[escapeLDAPFilter ${debugLabel}] Input:`, str);
const utf8Bytes = Buffer.from(str, 'utf8');
console.log(`[escapeLDAPFilter ${debugLabel}] UTF-8 Bytes:`, Array.from(utf8Bytes));
}
// Escape nur die speziellen LDAP-Filter-Zeichen
// UTF-8-Zeichen wie ß, ä, ö, ü werden direkt verwendet
return str
const escaped = str
.replace(/\\/g, '\\5c') // Backslash
.replace(/\*/g, '\\2a') // Stern
.replace(/\(/g, '\\28') // Öffnende Klammer
.replace(/\)/g, '\\29') // Schließende Klammer
.replace(/\0/g, '\\00'); // Null-Byte
if (debugLabel) {
console.log(`[escapeLDAPFilter ${debugLabel}] Output:`, escaped);
const escapedBytes = Buffer.from(escaped, 'utf8');
console.log(`[escapeLDAPFilter ${debugLabel}] Escaped UTF-8 Bytes:`, Array.from(escapedBytes));
}
return escaped;
}
/**
@@ -272,6 +287,22 @@ class LDAPService {
// Stelle sicher, dass Username als String behandelt wird (UTF-8 wird korrekt unterstützt)
const usernameStr = String(username || '').trim();
console.log('\n========== LDAP AUTHENTICATE - START ==========');
console.log('[LDAP] Username input:', usernameStr);
const utf8Bytes = Buffer.from(usernameStr, 'utf8');
console.log('[LDAP] Username UTF-8 Bytes:', Array.from(utf8Bytes));
console.log('[LDAP] Username UTF-8 Bytes (Hex):', Array.from(utf8Bytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '));
console.log('[LDAP] Username length:', usernameStr.length);
// Character-by-character analysis
console.log('[LDAP] Character analysis:');
for (let i = 0; i < usernameStr.length; i++) {
const char = usernameStr[i];
const codePoint = char.codePointAt(0);
const charBytes = Buffer.from(char, 'utf8');
console.log(` [${i}] "${char}" | U+${codePoint.toString(16).toUpperCase().padStart(4, '0')} (${codePoint}) | Bytes: [${Array.from(charBytes).join(', ')}]`);
}
if (!usernameStr) {
return callback(new Error('Benutzername darf nicht leer sein'), false);
}
@@ -282,24 +313,121 @@ class LDAPService {
return callback(new Error('LDAP ist nicht aktiviert'), false);
}
console.log('[LDAP] Config loaded - baseDN:', config.base_dn);
console.log('[LDAP] Config - usernameAttr:', config.username_attribute || 'sAMAccountName');
// LDAP-Verbindung herstellen (mit Service-Account)
this.connect(config, (err, client) => {
if (err) {
console.error('[LDAP] Connection error:', err);
return callback(err, false);
}
console.log('[LDAP] Connected successfully');
const baseDN = config.base_dn || '';
const usernameAttr = config.username_attribute || 'sAMAccountName';
// OR-Filter mit mehreren Varianten (exakt, lowercase, ß/ss), damit Login trotz unterschiedlicher Schreibweise funktioniert
const variants = this.getUsernameSearchVariants(usernameStr);
const filterParts = variants.map(v => `(${usernameAttr}=${this.escapeLDAPFilter(v)})`);
const searchFilter = filterParts.length === 1 ? filterParts[0] : `(|${filterParts.join('')})`;
const searchOptions = {
filter: searchFilter,
// Primär: Exakte Übereinstimmung mit sAMAccountName (wie in AD gespeichert)
// Varianten werden als Fallback verwendet, falls exakte Suche fehlschlägt
console.log('[LDAP] Using exact sAMAccountName match (as stored in AD)');
const exactEscaped = this.escapeLDAPFilter(usernameStr, 'exact');
const exactFilter = `(${usernameAttr}=${exactEscaped})`;
console.log('[LDAP] Exact LDAP Search Filter:', exactFilter);
console.log('[LDAP] Exact Filter UTF-8 Bytes:', Array.from(Buffer.from(exactFilter, 'utf8')));
const exactSearchOptions = {
filter: exactFilter,
scope: 'sub',
attributes: ['dn', usernameAttr]
};
console.log('[LDAP] Starting LDAP search with exact match...');
console.log('[LDAP] Search options:', JSON.stringify(exactSearchOptions, null, 2));
let userDN = null;
let canonicalUsername = null;
let searchAttempted = false;
// Zuerst exakte Suche versuchen
client.search(baseDN, exactSearchOptions, (err, res) => {
if (err) {
console.error('[LDAP] Exact search error:', err);
// Bei Fehler: Varianten als Fallback versuchen
return this.tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback);
}
searchAttempted = true;
res.on('searchEntry', (entry) => {
userDN = entry.dn.toString();
canonicalUsername = this.getAttributeValue(entry, usernameAttr) || usernameStr;
console.log('[LDAP] Exact search entry found!');
console.log('[LDAP] userDN:', userDN);
console.log('[LDAP] canonicalUsername:', canonicalUsername);
const canonicalBytes = Buffer.from(canonicalUsername, 'utf8');
console.log('[LDAP] canonicalUsername UTF-8 Bytes:', Array.from(canonicalBytes));
console.log('[LDAP] canonicalUsername UTF-8 Bytes (Hex):', Array.from(canonicalBytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '));
});
res.on('error', (err) => {
const errorMsg = err.message || String(err);
console.error('[LDAP] Exact search error:', errorMsg);
console.error('[LDAP] Error details:', err);
// Bei Fehler: Varianten als Fallback versuchen
if (!userDN) {
return this.tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback);
}
client.unbind();
callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false);
});
res.on('end', (result) => {
console.log('[LDAP] Exact search ended. Status:', result ? result.status : 'unknown');
console.log('[LDAP] userDN found:', userDN ? 'YES' : 'NO');
if (!userDN) {
console.log('[LDAP] Exact match not found, trying variants as fallback...');
// Exakte Suche fehlgeschlagen, Varianten als Fallback versuchen
return this.tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback);
}
// Exakte Suche erfolgreich - mit Bind fortfahren
this.performBind(config, userDN, password, canonicalUsername, callback);
});
});
});
});
}
/**
* Varianten-Suche als Fallback (wenn exakte Suche fehlschlägt)
*/
static tryVariantSearch(client, baseDN, usernameAttr, usernameStr, password, config, callback) {
console.log('[LDAP] Starting variant search as fallback...');
// OR-Filter mit mehreren Varianten (lowercase, ß/ss), damit Login trotz unterschiedlicher Schreibweise funktioniert
const variants = this.getUsernameSearchVariants(usernameStr);
console.log('[LDAP] Username variants generated:', variants);
console.log('[LDAP] Number of variants:', variants.length);
const filterParts = variants.map((v, idx) => {
const escaped = this.escapeLDAPFilter(v, `variant-${idx}`);
return `(${usernameAttr}=${escaped})`;
});
const searchFilter = filterParts.length === 1 ? filterParts[0] : `(|${filterParts.join('')})`;
console.log('[LDAP] Variant LDAP Search Filter:', searchFilter);
console.log('[LDAP] Variant Filter UTF-8 Bytes:', Array.from(Buffer.from(searchFilter, 'utf8')));
const searchOptions = {
filter: searchFilter,
scope: 'sub',
attributes: ['dn', usernameAttr]
};
let userDN = null;
let canonicalUsername = null;
@@ -314,44 +442,74 @@ class LDAPService {
userDN = entry.dn.toString();
// Kanonischen Benutzernamen aus LDAP verwenden (für DB-Lookup nach Sync)
canonicalUsername = this.getAttributeValue(entry, usernameAttr) || usernameStr;
console.log('[LDAP] Search entry found!');
console.log('[LDAP] userDN:', userDN);
console.log('[LDAP] canonicalUsername:', canonicalUsername);
const canonicalBytes = Buffer.from(canonicalUsername, 'utf8');
console.log('[LDAP] canonicalUsername UTF-8 Bytes:', Array.from(canonicalBytes));
console.log('[LDAP] canonicalUsername UTF-8 Bytes (Hex):', Array.from(canonicalBytes).map(b => '0x' + b.toString(16).padStart(2, '0')).join(' '));
});
res.on('error', (err) => {
client.unbind();
const errorMsg = err.message || String(err);
console.error('[LDAP] Search error:', errorMsg);
console.error('[LDAP] Error details:', err);
callback(new Error(`LDAP-Suchfehler: ${errorMsg}`), false);
});
res.on('end', (result) => {
client.unbind();
console.log('[LDAP] Search ended. Status:', result ? result.status : 'unknown');
console.log('[LDAP] userDN found:', userDN ? 'YES' : 'NO');
console.log('[LDAP] canonicalUsername:', canonicalUsername);
if (!userDN) {
console.error('[LDAP] User not found in LDAP (even with variants)!');
console.error('[LDAP] Searched for:', usernameStr);
console.error('[LDAP] Used filter:', searchFilter);
return callback(new Error(`Benutzer "${usernameStr}" nicht gefunden. Hinweis: Prüfen Sie, ob der Benutzername korrekt ist und UTF-8-Zeichen (wie ß, ä, ö, ü) korrekt geschrieben sind.`), false);
}
const authClient = ldap.createClient({
url: config.url,
timeout: 10000,
connectTimeout: 10000
});
authClient.on('error', (err) => {
authClient.unbind();
callback(err, false);
});
authClient.bind(userDN, password, (err) => {
authClient.unbind();
if (err) {
const errorMsg = err.message || String(err);
return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false);
}
// Erfolg: kanonischen Benutzernamen mitgeben, damit die DB-Lookup mit dem Sync-Benutzernamen funktioniert
callback(null, true, { username: canonicalUsername });
});
// Varianten-Suche erfolgreich - mit Bind fortfahren
this.performBind(config, userDN, password, canonicalUsername, callback);
});
});
});
}
/**
* LDAP Bind durchführen (Passwort-Authentifizierung)
*/
static performBind(config, userDN, password, canonicalUsername, callback) {
console.log('[LDAP] Attempting bind with userDN:', userDN);
const authClient = ldap.createClient({
url: config.url,
timeout: 10000,
connectTimeout: 10000
});
authClient.on('error', (err) => {
authClient.unbind();
console.error('[LDAP] Bind client error:', err);
callback(err, false);
});
authClient.bind(userDN, password, (err) => {
authClient.unbind();
if (err) {
const errorMsg = err.message || String(err);
console.error('[LDAP] Bind failed:', errorMsg);
console.error('[LDAP] Bind error details:', err);
return callback(new Error(`Ungültiges Passwort oder Authentifizierungsfehler: ${errorMsg}`), false);
}
// Erfolg: kanonischen Benutzernamen mitgeben, damit die DB-Lookup mit dem Sync-Benutzernamen funktioniert
console.log('[LDAP] Bind successful!');
console.log('[LDAP] Returning canonicalUsername:', canonicalUsername);
console.log('========== LDAP AUTHENTICATE - SUCCESS ==========\n');
callback(null, true, { username: canonicalUsername });
});
}