import { Client } from 'ldapts'; /** * Escape a value before putting it into an LDAP search filter (RFC 4515). * Prevents LDAP filter injection through the typed username. */ function escapeFilterValue(value) { return String(value).replace(/[\\*()]/g, (char) => { switch (char) { case '\\': return '\\5c'; case '*': return '\\2a'; case '(': return '\\28'; case ')': return '\\29'; default: return char; } }); } function buildFilter(template, username) { return template.replace(/\{\{username\}\}/g, escapeFilterValue(username)); } function firstValue(attributeValue) { if (Array.isArray(attributeValue)) { return attributeValue.length > 0 ? attributeValue[0].toString() : ''; } return attributeValue !== undefined && attributeValue !== null ? attributeValue.toString() : ''; } /** * Get the single-row LDAP settings, creating defaults on first access. */ async function getLdapSettings(LdapSetting) { let settings = await LdapSetting.findOne({ order: [['id', 'ASC']] }); if (!settings) { settings = await LdapSetting.create({}); } return settings; } /** * Authenticate a user against LDAP. * Binds with the service account, searches for the user, then binds as * the user to verify the password. Returns { email, username } on success. * Throws an Error with a human-readable message on any failure. */ async function authenticateLdap(settings, username, password) { // Reject empty password: many directories treat an empty password as an // anonymous (unauthenticated) bind, which would be an auth bypass. if (!password) { throw new Error('Password is required'); } if (!settings.url || !settings.searchBase) { throw new Error('LDAP is not configured'); } const client = new Client({ url: settings.url }); try { await client.bind(settings.bindDn, settings.bindCredentials); const filter = buildFilter(settings.searchFilter, username); const { searchEntries } = await client.search(settings.searchBase, { scope: 'sub', filter, attributes: [settings.emailAttribute, settings.usernameAttribute], }); if (searchEntries.length === 0) { throw new Error('User not found in directory'); } const entry = searchEntries[0]; // Verify the password by binding as the found user. await client.bind(entry.dn, password); const email = firstValue(entry[settings.emailAttribute]); if (!email) { throw new Error(`LDAP entry has no "${settings.emailAttribute}" attribute`); } const displayName = firstValue(entry[settings.usernameAttribute]) || email; return { email, username: displayName }; } finally { try { await client.unbind(); } catch { // ignore unbind errors } } } export { authenticateLdap, getLdapSettings, buildFilter, escapeFilterValue };