diff --git a/packages/xo-server-auth-ldap/README.md b/packages/xo-server-auth-ldap/README.md index 875437337..a6b825be9 100644 --- a/packages/xo-server-auth-ldap/README.md +++ b/packages/xo-server-auth-ldap/README.md @@ -19,10 +19,38 @@ of XO-Server: plugins: auth-ldap: - uri: "ldap://ldap.example.org", + uri: "ldap://ldap.example.org" + + # Credentials to use before looking for the user record. + # + # Default to anonymous. + bind: + + # Distinguished name of the user permitted to search the LDAP + # directory for the user to authenticate. + dn: 'cn=admin,ou=people,dc=example,dc=org' + + # Password of the user permitted to search the LDAP directory. + password: 'secret' + + # The base is the part of the directory tree where the users are + # looked for. base: "ou=people,dc=example,dc=org" + + # Filter used to find the user. + # + # Default is `'(uid={{name}})'`. + #filter: '(uid={{name}})' ``` +## Algorithm + +1. If `bind` is defined, attempt to bind using this user. +2. Searches for the user in the directory starting from the `base` + with the defined `filter`. +3. If found, a bind is attempted using the distinguished name of this + user and the provided password. + ## Development ### Installing dependencies diff --git a/packages/xo-server-auth-ldap/package.json b/packages/xo-server-auth-ldap/package.json index 902c7436a..ed69c21dc 100644 --- a/packages/xo-server-auth-ldap/package.json +++ b/packages/xo-server-auth-ldap/package.json @@ -25,6 +25,7 @@ "dependencies": { "babel-runtime": "^4", "bluebird": "^2.9.21", + "event-to-promise": "^0.3.2", "ldapjs": "^0.7.1" }, "devDependencies": { diff --git a/packages/xo-server-auth-ldap/src/index.js b/packages/xo-server-auth-ldap/src/index.js index a8279343d..a497555ee 100644 --- a/packages/xo-server-auth-ldap/src/index.js +++ b/packages/xo-server-auth-ldap/src/index.js @@ -1,40 +1,99 @@ -import Bluebird from 'bluebird' +import Bluebird, {coroutine, promisify} from 'bluebird' +import eventToPromise from 'event-to-promise' import {createClient} from 'ldapjs' import {escape} from 'ldapjs/lib/filters/escape' // =================================================================== +const VAR_RE = /\{\{([^}]+)\}\}/g +function evalFilter (filter, vars) { + return filter.replace(VAR_RE, (_, name) => { + const value = vars[name] + + if (value === undefined) { + throw new Error('invalid variable: ' + name) + } + + return escape(value) + }) +} + + function createAuthenticator (conf) { +} + +// =================================================================== + class AuthLdap { constructor (conf) { - const base = conf.base ? ',' + conf.base : '' const clientOpts = { - url: conf.uri + url: conf.uri, + maxConnections: 5 } - this._provider = (credentials) => { - const {username, password} = credentials + { + const {bind} = conf + if (bind) { + clientOpts.bindDN = bind.dn + clientOpts.bindCredentials = bind.password + } + } + + const {base: searchBase} = conf + const searchFilter = conf.filter || '(uid={{name}})' + + this._provider = coroutine(function * ({username, password}) { if (username === undefined || password === undefined) { - return Bluebird.reject(new Error('invalid credentials')) + throw null } - return new Bluebird((resolve, reject) => { - const client = createClient(clientOpts) + const client = createClient(clientOpts) - client.bind( - 'uid=' + escape(username) + base, - password, - (error) => { - if (error) { - reject(error) - } else { - resolve({ username }) - } + try { + // Promisify some methods. + const bind = promisify(client.bind, client) + const search = promisify(client.search, client) - client.unbind() + // Bind if necessary. + { + const {bind: credentials} = conf + if (credentials) { + yield bind(credentials.dn, credentials.password) } - ) - }) - } + } + + // Search for the user. + const entries = [] + { + const response = yield search(searchBase, { + scope: 'sub', + filter: evalFilter(searchFilter, { + name: username + }) + }) + + response.on('searchEntry', entry => { + entries.push(entry.json) + }) + + const {status} = yield eventToPromise(response, 'end') + if (status) { + throw new Error('unexpected search response status: ' + status) + } + } + + // Try to find an entry which can be bind with the given password. + for (let entry of entries) { + try { + yield bind(entry.objectName, password) + return { username } + } catch (error) {} + } + + throw null + } finally { + client.unbind() + } + }) } load (xo) {