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
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
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).