Make new LDAP options backward compatible

Also draft some kind of plugin system for auth, although it essentially consists in writing a function
and there is no mechanism to automatically fallback from one auth to another
This commit is contained in:
Élie Michel 2017-08-29 18:05:06 +02:00 committed by Elie Michel
parent 19710b90c0
commit cfa6db10c7
6 changed files with 232 additions and 142 deletions

View file

@ -371,15 +371,22 @@ module.exports = {
// 3. Lounge tries to connect a second time, but this time using the user's // 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. // DN and password. Auth is validated iff this connection is successful.
// //
// The search query takes a couple of parameters: // The search query takes a couple of parameters in `searchDN`:
// - A base DN. Only children nodes of this DN will be likely to be returned // - a base DN `searchDN/base`. Only children nodes of this DN will be likely
// - A search scope (see LDAP documentation) // to be returned;
// - The query itself, build as (&(<primaryKey>=<username>) <filter>) // - a search scope `searchDN/scope` (see LDAP documentation);
// - the query itself, build as (&(<primaryKey>=<username>) <filter>)
// where <username> is the user name provided in the log in request, // where <username> is the user name provided in the log in request,
// <primaryKey> is provided by the config and <fitler> is a filtering complement // <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 // also given in the config, to filter for instance only for nodes of type
// inetOrgPerson, or whatever LDAP search allows. // inetOrgPerson, or whatever LDAP search allows.
// //
// Alternatively, you can specify the `bindDN` parameter. This will make the lounge
// ignore searchDN options and assume that the user DN is always:
// <bindDN>,<primaryKey>=<username>
// where <username> is the user name provided in the log in request, and <bindDN>
// and <primaryKey> are provided by the config.
//
ldap: { ldap: {
// //
// Enable LDAP user authentication // Enable LDAP user authentication
@ -399,33 +406,23 @@ module.exports = {
// //
// LDAP connection tls options (only used if scheme is ldaps://) // LDAP connection tls options (only used if scheme is ldaps://)
// //
// @type object (see nodejs' tls.connect() options) // @type object (see nodejs' tls.connect() options)
// @default {} // @default {}
// //
// Example: // Example:
// You can use this option in order to force the use of IPv6: // You can use this option in order to force the use of IPv6:
// { // {
// host: 'my::ip::v6' // host: 'my::ip::v6',
// servername: 'ldaps://example.com' // servername: 'example.com'
// } // }
tlsOptions: {}, tlsOptions: {},
// //
// LDAP searching bind DN // LDAP base dn, alternative to searchDN
// 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
// //
rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com", // baseDN: "ou=accounts,dc=example,dc=com",
//
// Password of the lounge LDAP system user
//
// @type string
//
rootPassword: "1234",
// //
// LDAP primary key // LDAP primary key
@ -436,27 +433,55 @@ module.exports = {
primaryKey: "uid", primaryKey: "uid",
// //
// LDAP filter // LDAP search dn settings. This defines the procedure by which the
// lounge first look for user DN before authenticating her.
// Ignored if baseDN is specified
// //
// @type string // @type object
// @default "uid"
// //
filter: "(objectClass=inetOrgPerson)(memberOf=ou=accounts,dc=example,dc=com)", searchDN: {
// //
// LDAP search base (search only within this node) // LDAP searching bind DN
// // This bind DN is used to query the server for the DN of the user.
// @type string // 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.
base: "dc=example,dc=com", //
// @type string
//
rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com",
// //
// LDAP search scope // Password of the lounge LDAP system user
// //
// @type string // @type string
// @default "sub" //
// rootPassword: "1234",
scope: "sub"
//
// 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

@ -0,0 +1,29 @@
"use strict";
const Helper = require("../../helper");
const ldap = require("ldapjs");
function ldapAuthCommon(manager, client, user, bindDN, password, callback) {
const config = Helper.config;
let ldapclient = ldap.createClient({
url: config.ldap.url,
tlsOptions: config.ldap.tlsOptions
});
ldapclient.on("error", function(err) {
log.error("Unable to connect to LDAP server", err);
callback(!err);
});
ldapclient.bind(bindDN, password, function(err) {
if (!err && !client) {
manager.addUser(user, null);
}
ldapclient.unbind();
callback(!err);
});
}
module.exports = ldapAuthCommon;

View file

@ -0,0 +1,72 @@
"use strict";
const Helper = require("../../helper");
const ldap = require("ldapjs");
const _ldapAuthCommon = require("./_ldapCommon");
/**
* LDAP auth using initial DN search (see config comment for ldap.searchDN)
*/
function advancedLdapAuth(manager, client, user, password, callback) {
if (!user) {
return callback(false);
}
const config = Helper.config;
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
let ldapclient = ldap.createClient({
url: config.ldap.url,
tlsOptions: config.ldap.tlsOptions
});
const base = config.ldap.searchDN.base;
const searchOptions = {
scope: config.ldap.searchDN.scope,
filter: "(&(" + config.ldap.primaryKey + "=" + userDN + ")" + config.ldap.searchDN.filter + ")",
attributes: ["dn"]
};
ldapclient.on("error", function(err) {
log.error("Unable to connect to LDAP server", err);
callback(!err);
});
ldapclient.bind(config.ldap.searchDN.rootDN, config.ldap.searchDN.rootPassword, function(err) {
if (err) {
log.error("Invalid LDAP root credentials");
ldapclient.unbind();
callback(false);
} else {
ldapclient.search(base, searchOptions, function(err2, res) {
if (err2) {
log.warning("User not found: ", userDN);
ldapclient.unbind();
callback(false);
} else {
let found = false;
res.on("searchEntry", function(entry) {
found = true;
const bindDN = entry.objectName;
log.info("Auth against LDAP ", config.ldap.url, " with found bindDN ", bindDN);
ldapclient.unbind();
_ldapAuthCommon(manager, client, user, bindDN, password, callback);
});
res.on("error", function(err3) {
log.error("LDAP error: ", err3);
callback(false);
});
res.on("end", function() {
if (!found) {
callback(false);
}
});
}
});
}
});
}
module.exports = advancedLdapAuth;

21
src/plugins/auth/ldap.js Normal file
View file

@ -0,0 +1,21 @@
"use strict";
const Helper = require("../../helper");
const _ldapAuthCommon = require("./_ldapCommon");
function ldapAuth(manager, client, user, password, callback) {
if (!user) {
return callback(false);
}
const config = Helper.config;
const userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
const bindDN = config.ldap.primaryKey + "=" + userDN + "," + config.ldap.baseDN;
log.info("Auth against LDAP ", config.ldap.url, " with provided bindDN ", bindDN);
_ldapAuthCommon(manager, client, user, bindDN, password, callback);
}
module.exports = ldapAuth;

38
src/plugins/auth/local.js Normal file
View file

@ -0,0 +1,38 @@
"use strict";
const Helper = require("../../helper");
const colors = require("colors/safe");
function localAuth(manager, client, user, password, callback) {
// If no user is found, or if the client has not provided a password,
// fail the authentication straight away
if (!client || !password) {
return callback(false);
}
// If this user has no password set, fail the authentication
if (!client.config.password) {
log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`);
return callback(false);
}
Helper.password
.compare(password, client.config.password)
.then((matching) => {
if (matching && Helper.password.requiresUpdate(client.config.password)) {
const hash = Helper.password.hash(password);
client.setPassword(hash, (success) => {
if (success) {
log.info(`User ${colors.bold(client.name)} logged in and their hashed password has been updated to match new security requirements`);
}
});
}
callback(matching);
}).catch((error) => {
log.error(`Error while checking users password. Error: ${error}`);
});
}
module.exports = localAuth;

View file

@ -11,7 +11,9 @@ var path = require("path");
var io = require("socket.io"); var io = require("socket.io");
var dns = require("dns"); var dns = require("dns");
var Helper = require("./helper"); var Helper = require("./helper");
var ldap = require("ldapjs"); var ldapAuth = require("./plugins/auth/ldap");
var advancedLdapAuth = require("./plugins/auth/advancedLdap");
var localAuth = require("./plugins/auth/local");
var colors = require("colors/safe"); var colors = require("colors/safe");
const net = require("net"); const net = require("net");
const Identification = require("./identification"); const Identification = require("./identification");
@ -372,109 +374,6 @@ function initializeClient(socket, client, generateToken, token) {
} }
} }
function localAuth(client, user, password, callback) {
// If no user is found, or if the client has not provided a password,
// fail the authentication straight away
if (!client || !password) {
return callback(false);
}
// If this user has no password set, fail the authentication
if (!client.config.password) {
log.error(`User ${colors.bold(user)} with no local password set tried to sign in. (Probably a LDAP user)`);
return callback(false);
}
Helper.password
.compare(password, client.config.password)
.then((matching) => {
if (matching && Helper.password.requiresUpdate(client.config.password)) {
const hash = Helper.password.hash(password);
client.setPassword(hash, (success) => {
if (success) {
log.info(`User ${colors.bold(client.name)} logged in and their hashed password has been updated to match new security requirements`);
}
});
}
callback(matching);
}).catch((error) => {
log.error(`Error while checking users password. Error: ${error}`);
});
}
function ldapAuth(client, user, password, callback) {
if (!user) {
return callback(false);
}
var config = Helper.config;
var userDN = user.replace(/([,\\/#+<>;"= ])/g, "\\$1");
var ldapclient = ldap.createClient({
url: config.ldap.url,
tlsOptions: config.ldap.tlsOptions
});
var base = config.ldap.base;
var searchOptions = {
scope: config.ldap.scope,
filter: "(&(" + config.ldap.primaryKey + "=" + userDN + ")" + config.ldap.filter + ")",
attributes: ["dn"]
};
ldapclient.on("error", function(err) {
log.error("Unable to connect to LDAP server", err);
callback(!err);
});
ldapclient.bind(config.ldap.rootDN, config.ldap.rootPassword, function(err) {
if (err) {
log.error("Invalid LDAP root credentials");
ldapclient.unbind();
callback(false);
} else {
ldapclient.search(base, searchOptions, function(err2, res) {
if (err2) {
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 ", config.ldap.url, " with bindDN ", bindDN);
ldapclient.unbind();
var ldapclient2 = ldap.createClient({
url: config.ldap.url,
tlsOptions: config.ldap.tlsOptions
});
ldapclient2.bind(bindDN, password, function(err3) {
if (!err3 && !client) {
if (!manager.addUser(user, null)) {
log.error("Unable to create new user", user);
}
}
ldapclient2.unbind();
callback(!err3);
});
});
res.on("error", function(err3) {
log.error("LDAP error: ", err3);
callback(false);
});
res.on("end", function() {
if (!found) {
callback(false);
}
});
}
});
}
});
}
function performAuthentication(data) { function performAuthentication(data) {
const socket = this; const socket = this;
let client; let client;
@ -538,11 +437,17 @@ function performAuthentication(data) {
} }
// Perform password checking // Perform password checking
let auth = function() {};
if (!Helper.config.public && Helper.config.ldap.enable) { if (!Helper.config.public && Helper.config.ldap.enable) {
ldapAuth(client, data.user, data.password, authCallback); if ("baseDN" in Helper.config.ldap) {
auth = ldapAuth;
} else {
auth = advancedLdapAuth;
}
} else { } else {
localAuth(client, data.user, data.password, authCallback); auth = localAuth;
} }
auth(manager, client, data.user, data.password, authCallback);
} }
function reverseDnsLookup(ip, callback) { function reverseDnsLookup(ip, callback) {