ldapjs Examples

This page contains a (hopefully) growing list of sample code to get you started with ldapjs.

In-memory server

const ldap = require('ldapjs');


///--- Shared handlers

function authorize(req, res, next) {
  /* Any user may search after bind, only cn=root has full power */
  const isSearch = (req instanceof ldap.SearchRequest);
  if (!req.connection.ldap.bindDN.equals('cn=root') && !isSearch)
    return next(new ldap.InsufficientAccessRightsError());

  return next();
}


///--- Globals

const SUFFIX = 'o=joyent';
const db = {};
const server = ldap.createServer();



server.bind('cn=root', (req, res, next) => {
  if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
    return next(new ldap.InvalidCredentialsError());

  res.end();
  return next();
});

server.add(SUFFIX, authorize, (req, res, next) => {
  const dn = req.dn.toString();

  if (db[dn])
    return next(new ldap.EntryAlreadyExistsError(dn));

  db[dn] = req.toObject().attributes;
  res.end();
  return next();
});

server.bind(SUFFIX, (req, res, next) => {
  const dn = req.dn.toString();
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  if (!db[dn].userpassword)
    return next(new ldap.NoSuchAttributeError('userPassword'));

  if (db[dn].userpassword.indexOf(req.credentials) === -1)
    return next(new ldap.InvalidCredentialsError());

  res.end();
  return next();
});

server.compare(SUFFIX, authorize, (req, res, next) => {
  const dn = req.dn.toString();
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  if (!db[dn][req.attribute])
    return next(new ldap.NoSuchAttributeError(req.attribute));

  const matches = false;
  const vals = db[dn][req.attribute];
  for (const value of vals) {
    if (value === req.value) {
      matches = true;
      break;
    }
  }

  res.end(matches);
  return next();
});

server.del(SUFFIX, authorize, (req, res, next) => {
  const dn = req.dn.toString();
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  delete db[dn];

  res.end();
  return next();
});

server.modify(SUFFIX, authorize, (req, res, next) => {
  const dn = req.dn.toString();
  if (!req.changes.length)
    return next(new ldap.ProtocolError('changes required'));
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  const entry = db[dn];

  for (const change of req.changes) {
    mod = change.modification;
    switch (change.operation) {
    case 'replace':
      if (!entry[mod.type])
        return next(new ldap.NoSuchAttributeError(mod.type));

      if (!mod.vals || !mod.vals.length) {
        delete entry[mod.type];
      } else {
        entry[mod.type] = mod.vals;
      }

      break;

    case 'add':
      if (!entry[mod.type]) {
        entry[mod.type] = mod.vals;
      } else {
        for (const v of mod.vals) {
          if (entry[mod.type].indexOf(v) === -1)
            entry[mod.type].push(v);
        }
      }

      break;

    case 'delete':
      if (!entry[mod.type])
        return next(new ldap.NoSuchAttributeError(mod.type));

      delete entry[mod.type];

      break;
    }
  }

  res.end();
  return next();
});

server.search(SUFFIX, authorize, (req, res, next) => {
  const dn = req.dn.toString();
  if (!db[dn])
    return next(new ldap.NoSuchObjectError(dn));

  let scopeCheck;

  switch (req.scope) {
  case 'base':
    if (req.filter.matches(db[dn])) {
      res.send({
        dn: dn,
        attributes: db[dn]
      });
    }

    res.end();
    return next();

  case 'one':
    scopeCheck = (k) => {
      if (req.dn.equals(k))
        return true;

      const parent = ldap.parseDN(k).parent();
      return (parent ? parent.equals(req.dn) : false);
    };
    break;

  case 'sub':
    scopeCheck = (k) => {
      return (req.dn.equals(k) || req.dn.parentOf(k));
    };

    break;
  }

  const keys = Object.keys(db);
  for (const key of keys) {
    if (!scopeCheck(key))
      return;

    if (req.filter.matches(db[key])) {
      res.send({
        dn: key,
        attributes: db[key]
      });
    }
  }

  res.end();
  return next();
});



///--- Fire it up

server.listen(1389, () => {
  console.log('LDAP server up at: %s', server.url);
});

/etc/passwd server

const fs = require('fs');
const ldap = require('ldapjs');
const { spawn } = require('child_process');



///--- Shared handlers

function authorize(req, res, next) {
  if (!req.connection.ldap.bindDN.equals('cn=root'))
    return next(new ldap.InsufficientAccessRightsError());

  return next();
}


function loadPasswdFile(req, res, next) {
  fs.readFile('/etc/passwd', 'utf8', (err, data) => {
    if (err)
      return next(new ldap.OperationsError(err.message));

    req.users = {};

    const lines = data.split('\n');
    for (const line of lines) {
      if (!line || /^#/.test(line))
        continue;

      const record = line.split(':');
      if (!record || !record.length)
        continue;

      req.users[record[0]] = {
        dn: 'cn=' + record[0] + ', ou=users, o=myhost',
        attributes: {
          cn: record[0],
          uid: record[2],
          gid: record[3],
          description: record[4],
          homedirectory: record[5],
          shell: record[6] || '',
          objectclass: 'unixUser'
        }
      };
    }

    return next();
  });
}


const pre = [authorize, loadPasswdFile];



///--- Mainline

const server = ldap.createServer();

server.bind('cn=root', (req, res, next) => {
  if (req.dn.toString() !== 'cn=root' || req.credentials !== 'secret')
    return next(new ldap.InvalidCredentialsError());

  res.end();
  return next();
});


server.add('ou=users, o=myhost', pre, (req, res, next) => {
  if (!req.dn.rdns[0].cn)
    return next(new ldap.ConstraintViolationError('cn required'));

  if (req.users[req.dn.rdns[0].cn])
    return next(new ldap.EntryAlreadyExistsError(req.dn.toString()));

  const entry = req.toObject().attributes;

  if (entry.objectclass.indexOf('unixUser') === -1)
    return next(new ldap.ConstraintViolationError('entry must be a unixUser'));

  const opts = ['-m'];
  if (entry.description) {
    opts.push('-c');
    opts.push(entry.description[0]);
  }
  if (entry.homedirectory) {
    opts.push('-d');
    opts.push(entry.homedirectory[0]);
  }
  if (entry.gid) {
    opts.push('-g');
    opts.push(entry.gid[0]);
  }
  if (entry.shell) {
    opts.push('-s');
    opts.push(entry.shell[0]);
  }
  if (entry.uid) {
    opts.push('-u');
    opts.push(entry.uid[0]);
  }
  opts.push(entry.cn[0]);
  const useradd = spawn('useradd', opts);

  const messages = [];

  useradd.stdout.on('data', (data) => {
    messages.push(data.toString());
  });
  useradd.stderr.on('data', (data) => {
    messages.push(data.toString());
  });

  useradd.on('exit', (code) => {
    if (code !== 0) {
      let msg = '' + code;
      if (messages.length)
        msg += ': ' + messages.join();
      return next(new ldap.OperationsError(msg));
    }

    res.end();
    return next();
  });
});


server.modify('ou=users, o=myhost', pre, (req, res, next) => {
  if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
    return next(new ldap.NoSuchObjectError(req.dn.toString()));

  if (!req.changes.length)
    return next(new ldap.ProtocolError('changes required'));

  const user = req.users[req.dn.rdns[0].cn].attributes;
  let mod;

  for (const change of req.changes) {
    mod = change.modification;
    switch (change.operation) {
    case 'replace':
      if (mod.type !== 'userpassword' || !mod.vals || !mod.vals.length)
        return next(new ldap.UnwillingToPerformError('only password updates ' +
                                                     'allowed'));
      break;
    case 'add':
    case 'delete':
      return next(new ldap.UnwillingToPerformError('only replace allowed'));
    }
  }

  const passwd = spawn('chpasswd', ['-c', 'MD5']);
  passwd.stdin.end(user.cn + ':' + mod.vals[0], 'utf8');

  passwd.on('exit', (code) => {
    if (code !== 0)
      return next(new ldap.OperationsError('' + code));

    res.end();
    return next();
  });
});


server.del('ou=users, o=myhost', pre, (req, res, next) => {
  if (!req.dn.rdns[0].cn || !req.users[req.dn.rdns[0].cn])
    return next(new ldap.NoSuchObjectError(req.dn.toString()));

  const userdel = spawn('userdel', ['-f', req.dn.rdns[0].cn]);

  const messages = [];
  userdel.stdout.on('data', (data) => {
    messages.push(data.toString());
  });
  userdel.stderr.on('data', (data) => {
    messages.push(data.toString());
  });

  userdel.on('exit', (code) => {
    if (code !== 0) {
      let msg = '' + code;
      if (messages.length)
        msg += ': ' + messages.join();
      return next(new ldap.OperationsError(msg));
    }

    res.end();
    return next();
  });
});


server.search('o=myhost', pre, (req, res, next) => {
  const keys = Object.keys(req.users);
  for (const k of keys) {
    if (req.filter.matches(req.users[k].attributes))
      res.send(req.users[k]);
  }

  res.end();
  return next();
});



// LDAP "standard" listens on 389, but whatever.
server.listen(1389, '127.0.0.1', () => {
  console.log('/etc/passwd LDAP server up at: %s', server.url);
});

Address Book

This example is courtesy of Diogo Resende and illustrates setting up an address book for typical mail clients such as Thunderbird or Evolution over a MySQL database.

// MySQL test: (create on database 'abook' with username 'abook' and password 'abook')
//
// CREATE TABLE IF NOT EXISTS `users` (
//   `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
//   `username` varchar(50) NOT NULL,
//   `password` varchar(50) NOT NULL,
//   PRIMARY KEY (`id`),
//   KEY `username` (`username`)
// ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
// INSERT INTO `users` (`username`, `password`) VALUES
// ('demo', 'demo');
// CREATE TABLE IF NOT EXISTS `contacts` (
//   `id` int(5) unsigned NOT NULL AUTO_INCREMENT,
//   `user_id` int(5) unsigned NOT NULL,
//   `name` varchar(100) NOT NULL,
//   `email` varchar(255) NOT NULL,
//   PRIMARY KEY (`id`),
//   KEY `user_id` (`user_id`)
// ) ENGINE=InnoDB  DEFAULT CHARSET=utf8;
// INSERT INTO `contacts` (`user_id`, `name`, `email`) VALUES
// (1, 'John Doe', 'john.doe@example.com'),
// (1, 'Jane Doe', 'jane.doe@example.com');
//

const ldap = require('ldapjs');
const mysql = require("mysql");
const server = ldap.createServer();
const addrbooks = {};
const userinfo = {};
const ldap_port = 389;
const basedn = "dc=example, dc=com";
const company = "Example";
const db = mysql.createClient({
  user: "abook",
  password: "abook",
  database: "abook"
});

db.query("SELECT c.*,u.username,u.password " +
         "FROM contacts c JOIN users u ON c.user_id=u.id",
         (err, contacts) => {
  if (err) {
    console.log("Error fetching contacts", err);
    process.exit(1);
  }

  for (const contact of contacts) {
    if (!addrbooks.hasOwnProperty(contact.username)) {
      addrbooks[contact.username] = [];
      userinfo["cn=" + contact.username + ", " + basedn] = {
        abook: addrbooks[contact.username],
        pwd: contact.password
      };
    }

    const p = contact.name.indexOf(" ");
    if (p != -1)
      contact.firstname = contact.name.substr(0, p);

    p = contact.name.lastIndexOf(" ");
    if (p != -1)
      contact.surname = contact.name.substr(p + 1);

    addrbooks[contact.username].push({
      dn: "cn=" + contact.name + ", " + basedn,
      attributes: {
        objectclass: [ "top" ],
        cn: contact.name,
        mail: contact.email,
        givenname: contact.firstname,
        sn: contact.surname,
        ou: company
      }
    });
  }

  server.bind(basedn, (req, res, next) => {
    const username = req.dn.toString();
    const password = req.credentials;

    if (!userinfo.hasOwnProperty(username) ||
         userinfo[username].pwd != password) {
      return next(new ldap.InvalidCredentialsError());
    }

    res.end();
    return next();
  });

  server.search(basedn, (req, res, next) => {
    const binddn = req.connection.ldap.bindDN.toString();

    if (userinfo.hasOwnProperty(binddn)) {
      for (const abook of userinfo[binddn].abook) {
        if (req.filter.matches(abook.attributes))
          res.send(abook);
      }
    }
    res.end();
  });

  server.listen(ldap_port, () => {
    console.log("Addressbook started at %s", server.url);
  });
});

To test out this example, try:

$ ldapsearch -H ldap://localhost:389 -x -D cn=demo,dc=example,dc=com \
  -w demo -b "dc=example,dc=com" objectclass=*

Multi-threaded Server

This example demonstrates multi-threading via the cluster module utilizing a net server for initial socket receipt. An alternate example demonstrating use of the connectionRouter serverOptions hook is available in the examples directory.

const cluster = require('cluster');
const ldap = require('ldapjs');
const net = require('net');
const os = require('os');

const threads = [];
threads.getNext = function () {
    return (Math.floor(Math.random() * this.length));
};

const serverOptions = {
    port: 1389
};

if (cluster.isMaster) {
    const server = net.createServer(serverOptions, (socket) => {
        socket.pause();
        console.log('ldapjs client requesting connection');
        let routeTo = threads.getNext();
        threads[routeTo].send({ type: 'connection' }, socket);
    });

    for (let i = 0; i < os.cpus().length; i++) {
        let thread = cluster.fork({
            'id': i
        });
        thread.id = i;
        thread.on('message', function (msg) {

        });
        threads.push(thread);
    }

    server.listen(serverOptions.port, function () {
        console.log('ldapjs listening at ldap://127.0.0.1:' + serverOptions.port);
    });
} else {
    const server = ldap.createServer(serverOptions);

    let threadId = process.env.id;

    process.on('message', (msg, socket) => {
        switch (msg.type) {
            case 'connection':
                server.newConnection(socket);
                socket.resume();
                console.log('ldapjs client connection accepted on ' + threadId.toString());
        }
    });

    server.search('dc=example', function (req, res, next) {
        console.log('ldapjs search initiated on ' + threadId.toString());
        var obj = {
            dn: req.dn.toString(),
            attributes: {
                objectclass: ['organization', 'top'],
                o: 'example'
            }
        };

        if (req.filter.matches(obj.attributes))
            res.send(obj);

        res.end();
    });
}