Browse project docs

In-memory Server Example

Run a complete ldapjs-community LDAP server with bind authorization, scoped search, errors, and shutdown handling.

On this page

This example creates a small LDAP server for local development. It accepts one configured bind identity and serves one in-memory entry through base, one-level, or subtree searches.

Local development only

The default listener is loopback-only and uses plaintext LDAP. Do not expose it to a network or send production credentials through it. A deployed server needs TLS, access controls, persistence, resource limits, logging, and application-specific validation.

Link to EnvironmentEnvironment

bash
export LDAP_SERVER_HOST='127.0.0.1'
export LDAP_SERVER_PORT='1389'
export LDAP_BASE_DN='dc=example,dc=com'
export LDAP_BIND_DN='cn=admin,dc=example,dc=com'
export LDAP_PASSWORD='replace-me'

Link to CodeCode

javascript
const ldap = require('ldapjs')

for (const name of ['LDAP_BASE_DN', 'LDAP_BIND_DN', 'LDAP_PASSWORD']) {
  if (!process.env[name]) throw new Error(`Missing ${name}`)
}

const host = process.env.LDAP_SERVER_HOST || '127.0.0.1'
const port = Number(process.env.LDAP_SERVER_PORT || 1389)
if (!Number.isInteger(port) || port < 1 || port > 65535) {
  throw new Error('LDAP_SERVER_PORT must be an integer from 1 to 65535')
}

const baseDN = ldap.parseDN(process.env.LDAP_BASE_DN)
const bindDN = ldap.parseDN(process.env.LDAP_BIND_DN)
const password = process.env.LDAP_PASSWORD

const entries = [{
  dn: bindDN,
  attributes: {
    objectclass: ['top', 'person'],
    cn: ['admin'],
    sn: ['administrator']
  }
}]

function authorize(req, res, next) {
  if (!req.connection.ldap.bindDN.equals(bindDN)) {
    return next(new ldap.InsufficientAccessRightsError())
  }
  return next()
}

function isInScope(searchBase, entryDN, scope) {
  if (scope === 'base') return searchBase.equals(entryDN)
  if (scope === 'one') {
    const parent = entryDN.parent()
    return parent ? parent.equals(searchBase) : false
  }
  if (scope === 'sub') return searchBase.equals(entryDN) || searchBase.parentOf(entryDN)
  return false
}

const server = ldap.createServer()

server.on('clientError', (err) => {
  console.error('LDAP client error:', err)
})

server.on('error', (err) => {
  console.error('LDAP server error:', err)
  process.exitCode = 1
})

server.bind(baseDN.toString(), (req, res, next) => {
  if (!req.dn.equals(bindDN) || req.credentials !== password) {
    return next(new ldap.InvalidCredentialsError())
  }

  res.end()
  return next()
})

server.search(baseDN.toString(), authorize, (req, res, next) => {
  for (const entry of entries) {
    if (isInScope(req.dn, entry.dn, req.scope) && req.filter.matches(entry.attributes)) {
      res.send({
        dn: entry.dn.toString(),
        attributes: entry.attributes
      })
    }
  }

  res.end()
  return next()
})

server.listen(port, host, () => {
  console.log(`LDAP server listening at ${server.url}`)
})

function shutdown() {
  server.close(() => {
    console.log('LDAP server stopped')
  })
}

process.once('SIGINT', shutdown)
process.once('SIGTERM', shutdown)

Run node server.js, then connect with an LDAP client using the configured bind DN and password. Search from LDAP_BASE_DN with an appropriate scope and filter such as (objectClass=person).