New implementation.
This commit is contained in:
parent
4617025bd4
commit
f2e7963e1f
@ -19,10 +19,38 @@ of XO-Server:
|
|||||||
plugins:
|
plugins:
|
||||||
|
|
||||||
auth-ldap:
|
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"
|
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
|
## Development
|
||||||
|
|
||||||
### Installing dependencies
|
### Installing dependencies
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"babel-runtime": "^4",
|
"babel-runtime": "^4",
|
||||||
"bluebird": "^2.9.21",
|
"bluebird": "^2.9.21",
|
||||||
|
"event-to-promise": "^0.3.2",
|
||||||
"ldapjs": "^0.7.1"
|
"ldapjs": "^0.7.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -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 {createClient} from 'ldapjs'
|
||||||
import {escape} from 'ldapjs/lib/filters/escape'
|
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 {
|
class AuthLdap {
|
||||||
constructor (conf) {
|
constructor (conf) {
|
||||||
const base = conf.base ? ',' + conf.base : ''
|
|
||||||
const clientOpts = {
|
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) {
|
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(
|
try {
|
||||||
'uid=' + escape(username) + base,
|
// Promisify some methods.
|
||||||
password,
|
const bind = promisify(client.bind, client)
|
||||||
(error) => {
|
const search = promisify(client.search, client)
|
||||||
if (error) {
|
|
||||||
reject(error)
|
|
||||||
} else {
|
|
||||||
resolve({ username })
|
|
||||||
}
|
|
||||||
|
|
||||||
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) {
|
load (xo) {
|
||||||
|
Loading…
Reference in New Issue
Block a user