How to Search LDAP with Node.js

Connect, bind, and search an LDAP directory from Node.js with escaped filters, bounded result handling, TLS validation, and reliable cleanup.

On this page

Why Search LDAP with Node.js?

LDAP search is at the heart of authenticating users, checking group membership, and integrating Node.js apps with enterprise directories such as Active Directory. Unlike SQL, LDAP search relies on strict, standardized filter syntax and requires careful attention to directory structure, attribute naming, and secure input handling. Searching (retrieving user or group records) and authenticating (bind to verify credentials) are distinct processes; both are often required, but their code paths and error modes differ.

A correct LDAP search in Node.js demands:

  • The base DN (distinguished name): the subtree root under which to search.
  • A correctly-formed filter, built using RFC 4515 syntax—not ad hoc interpolated strings.
  • Scope, controlling how broadly to search (base, one, or sub).
  • A list of attributes to retrieve from each found entry.

Many errors—silent search failures, mismatched results, or subtle security flaws—trace back to these fundamentals. Safe Node.js searches also require a maintained client library, bounded result handling, escaped input, and secure connections.

Choosing a Node.js LDAP Library: ldapjs vs. ldapts

Modern Node.js projects should generally use one of two client libraries:

  • ldapjs: Once standard, but now archived and not maintained as of 2024. Best avoided for new code due to compatibility gaps and unresolved bugs.
  • ldapts: The actively maintained library supporting promises and async/await. Written in TypeScript, it supports both JavaScript (ESM or CommonJS) and TypeScript projects. Prefer ldapts for new LDAP search code.

Install the ldapts package with:

bash
npm install ldapts

For code samples, use either CommonJS (const { Client } = require('ldapts')) or ESM/TypeScript (import { Client } from 'ldapts'). The core API is the same; examples below use CommonJS, but TypeScript users can directly use the import form without changes to the underlying method calls.

Always check which library a sample uses: older ldapjs code may not work in current Node.js versions and lacks recent security fixes.

LDAP Search Workflow in Node.js Applications

The robust LDAP search workflow includes these critical steps:

  1. Connect: Establish connection to the LDAP server via host and port (with optional TLS/SSL).
  2. Bind: Authenticate with a service (or user) DN and password. The bound user’s rights control search capabilities.
  3. Search: Execute a search under the base DN, specifying scope, attributes, and a safe, properly-escaped filter string.
  4. Process Results: Loop over or analyze matching entries.
  5. Unbind/Close: Disconnect from the LDAP server to free resources.

Always predefine server details, credentials, and the right search base for your intended scope. In large directories, paged search is essential—otherwise, the server might silently truncate results.

A minimalist search operation with ldapts:

javascript
// CommonJS: const { Client } = require('ldapts');
// TypeScript/ESM: import { Client } from 'ldapts';

const { Client } = require('ldapts');

const url = 'ldap://your-ldap-server';
const bindDN = 'cn=admin,dc=example,dc=com';
const bindPassword = 'securepassword';
const searchBase = 'ou=users,dc=example,dc=com';
const username = 'jdoe';
const attributes = ['dn', 'mail', 'cn', 'uid'];

// RFC 4515 escaping for user input:
function escapeLDAP(input) {
  return input.replace(/[\0()*\\]/g, (c) =>
    ({
      '\\': '\\5c',
      '*': '\\2a',
      '(': '\\28',
      ')': '\\29',
      '\0': '\\00',
    }[c])
  );
}
const filter = `(uid=${escapeLDAP(username)})`;

(async () => {
  const client = new Client({ url });

  try {
    await client.bind(bindDN, bindPassword);

    const { searchEntries } = await client.search(searchBase, {
      scope: 'sub',
      filter,
      attributes,
      paged: true,
    });

    for (const entry of searchEntries) {
      // entry.dn, entry.mail, etc.
    }
  } catch (err) {
    // See safe error logging below.
  } finally {
    await client.unbind();
  }
})();

For TypeScript or ES modules, replace the require with import { Client } from 'ldapts'. Inline comments clarify each phase of the workflow. Never print secrets or unredacted user data in logs.

Mastering LDAP Search Filters: RFC 4515 Essentials

LDAP search filters precisely define what the server matches and returns. The grammar (parentheses, operators, attribute types, escaping rules) is standardized by RFC 4515. Filters must be strictly valid—syntax errors or bad escaping will cause empty results or outright failures.

Examples:

  • (uid=jdoe) — Find user with login name “jdoe”.
  • (&(objectClass=user)(mail=*)) — All user objects where email exists.
  • (|(sAMAccountName=jsmith)(userPrincipalName=jsmith@domain.com)) — User with either login or UPN.

Logical operators:

  • AND: (&(condition1)(condition2))
  • OR: (|(testA)(testB))
  • NOT: (!(negativeCondition))

Escaping User Input: Preventing Injection and Syntax Errors

User-supplied values in filters are a main attack surface and source of errors. Never interpolate raw user input as filter values. Always escape special characters per RFC 4515—*, (, ), \, and null bytes—using hex encoding.

A compliant Node.js example:

javascript
// RFC 4515 escaping for filter values
function escapeLDAP(value) {
  return value.replace(/[\0()*\\]/g, (char) =>
    ({
      '\\': '\\5c',
      '*': '\\2a',
      '(': '\\28',
      ')': '\\29',
      '\0': '\\00',
    })[char]
  );
}

// Safe filter construction
const safeValue = escapeLDAP(userInput);
const filter = `(uid=${safeValue})`;

Use this function any time user input (such as usernames, emails, or search queries) enters a filter. Do not rely on library ‘convenience’—ldapts does not currently offer built-in escaping.

Common Filter Construction Errors

  • Not escaping input correctly allows filter injection or search failures.
  • Bad parentheses and syntax errors silently yield empty results.
  • Mistaking CLI-tested filters (“they work in ldapsearch”) for safely coded filters—always escape programmatic input.

Validate all filters, especially when user-controlled, before executing LDAP searches in production.

Handling Results, Errors, and Schema Gotchas

Even with correct syntax, searches may return incomplete, empty, or inconsistent results. Typical causes include:

  • Wrong base DN or scope: A filter matching CLI output may do nothing if the search is rooted at the wrong tree point.
  • Schema and attribute name mismatches: Not all directories use uid for usernames—Active Directory may require sAMAccountName, while others use uid or cn.
  • Attributes not populated for a given entry: Filtering on a field that is unset means no returns.
  • Directory flavor differences: AD, OpenLDAP, and vendor directories often vary in required objectClass, case sensitivity, and permissible attributes.

Common errors:

  • Empty result set: Likely due to search base, scope, or filter issues.
  • Bind failures: Invalid credentials or insufficient privileges.
  • Protocol errors: Usually from malformed filters; likely unescaped input or broken grouping.

Safe Error Logging Example

Never log sensitive data such as user bind credentials, unescaped user input, or complete distinguished names in production logs. Redact user input and filter details before logging. For example:

javascript
catch (err) {
  // Redact sensitive information before logging
  console.error(
    `LDAP search failed: ${err.message}`,
    {
      searchBase: '[REDACTED]',
      filter: '[REDACTED]',
    }
  );
}

Always ensure production logs avoid exposing user attributes, credentials, or any personally identifiable data. Provide only information necessary for diagnosing the class of error.

Security and Robustness: Escaping, Credential Handling, Testing

Secure LDAP code always strictly escapes user input (per above). Never interpolate outside-controlled data directly into filters, and do not assume libraries handle escaping for you unless explicitly documented.

Credential best practices:

  • Use service accounts with minimum necessary privileges for search.
  • Never log or expose bind DNs or passwords, even on error.

Testing and production advice:

  • Use a test/dummy directory during development to verify filter behavior without risking real data.
  • Rely on paged search to avoid missing results in large directories.
  • For high-frequency or concurrent search operations, explore connection pooling where supported.

Further Reading: Standards and Official LDAP References

For full details on correct filter syntax, escaping, and directory search options, always consult the primary sources:

  • RFC 4515 for filter grammar and escaping
  • RFC 2254 for historical and some legacy codebases
  • Official vendor (e.g., Oracle, Microsoft) documentation for schema specifics and filter patterns

These standards are the final authority; troubleshooting should always begin by validating against these references.

Sources