Implement a proper LDAP authentication process

The Lounge first log as a special user in order to search (as in LDAP's
'"search" verb) for the user's full DN. It then attempts to bind using the
found user DN and the user provided password.
This commit is contained in:
Élie Michel 2017-03-21 15:15:33 +01:00
parent 52cc3ee909
commit 642442c041
2 changed files with 123 additions and 12 deletions

View File

@ -329,6 +329,23 @@ module.exports = {
// @type object // @type object
// @default {} // @default {}
// //
// The authentication process works as follows:
//
// 1. Lounge connects to the LDAP server with its system credentials
// 2. It performs a LDAP search query to find the full DN associated to the
// user requesting to log in.
// 3. Lounge tries to connect a second time, but this time using the user's
// DN and password. Auth is validated iff this connection is successful.
//
// The search query takes a couple of parameters:
// - A base DN. Only children nodes of this DN will be likely to be returned
// - A search scope (see LDAP documentation)
// - The query itself, build as (&(<primaryKey>=<username>) <filter>)
// where <username> is the user name provided in the log in request,
// <primaryKey> is provided by the config and <fitler> is a filtering complement
// also given in the config, to filter for instance only for nodes of type
// inetOrgPerson, or whatever LDAP search allows.
//
ldap: { ldap: {
// //
// Enable LDAP user authentication // Enable LDAP user authentication
@ -346,11 +363,35 @@ module.exports = {
url: "ldaps://example.com", url: "ldaps://example.com",
// //
// LDAP base dn // LDAP connection tls options (only used if scheme is ldaps://)
//
// @type object (see nodejs' tls.connect() options)
// @default {}
//
// Example:
// You can use this option in order to force the use of IPv6:
// {
// host: 'my::ip::v6'
// servername: 'ldaps://example.com'
// }
tlsOptions: {},
//
// LDAP searching bind DN
// This bind DN is used to query the server for the DN of the user.
// This is supposed to be a system user that has access in read only to
// the DNs of the people that are allowed to log in.
// //
// @type string // @type string
// //
baseDN: "ou=accounts,dc=example,dc=com", rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com",
//
// Password of the lounge LDAP system user
//
// @type string
//
rootPassword: "1234",
// //
// LDAP primary key // LDAP primary key
@ -358,7 +399,30 @@ module.exports = {
// @type string // @type string
// @default "uid" // @default "uid"
// //
primaryKey: "uid" primaryKey: "uid",
//
// LDAP filter
//
// @type string
// @default "uid"
//
filter: "(objectClass=inetOrgPerson)(memberOf=ou=accounts,dc=example,dc=com)",
//
// LDAP search base (search only within this node)
//
// @type string
//
base: "dc=example,dc=com",
//
// LDAP search scope
//
// @type string
// @default "sub"
//
scope: "sub"
}, },
// Extra debugging // Extra debugging

View File

@ -283,27 +283,74 @@ function localAuth(client, user, password, callback) {
} }
function ldapAuth(client, user, password, callback) { function ldapAuth(client, user, password, callback) {
if (!user) {
return callback(false);
}
var userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1"); var userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
var bindDN = Helper.config.ldap.primaryKey + "=" + userDN + "," + Helper.config.ldap.baseDN;
var ldapclient = ldap.createClient({ var ldapclient = ldap.createClient({
url: Helper.config.ldap.url url: Helper.config.ldap.url,
tlsOptions: Helper.config.ldap.tlsOptions
}); });
var base = Helper.config.ldap.base;
var searchOptions = {
scope: Helper.config.ldap.scope,
filter: '(&(' + Helper.config.ldap.primaryKey + '=' + userDN + ')' + Helper.config.ldap.filter + ')',
attributes: ['dn']
};
ldapclient.on("error", function(err) { ldapclient.on("error", function(err) {
log.error("Unable to connect to LDAP server", err); log.error("Unable to connect to LDAP server", err);
callback(!err); callback(!err);
}); });
ldapclient.bind(bindDN, password, function(err) { ldapclient.bind(Helper.config.ldap.rootDN,
Helper.config.ldap.rootPassword,
function(err) {
if (err) {
log.error("Invalid LDAP root credentials");
ldapclient.unbind();
callback(false);
} else {
ldapclient.search(base, searchOptions, function(err, res) {
if (err) {
log.warning("User not found: ", userDN);
ldapclient.unbind();
callback(false);
} else {
var found = false;
res.on('searchEntry', function(entry) {
found = true;
var bindDN = entry.objectName;
log.info("Auth against LDAP ", Helper.config.ldap.url, " with bindDN ", bindDN);
ldapclient.unbind()
var ldapclient2 = ldap.createClient({
url: Helper.config.ldap.url,
tlsOptions: Helper.config.ldap.tlsOptions
});
ldapclient2.bind(bindDN, password, function(err) {
if (!err && !client) { if (!err && !client) {
if (!manager.addUser(user, null)) { if (!manager.addUser(user, null)) {
log.error("Unable to create new user", user); log.error("Unable to create new user", user);
} }
} }
ldapclient.unbind(); ldapclient2.unbind();
callback(!err); callback(!err);
}); });
});
res.on('error', function(resErr) {
callback(false);
});
res.on('end', function(result) {
if (!found) {
callback(false);
}
});
}
});
}
});
} }
function auth(data) { function auth(data) {