diff --git a/defaults/config.js b/defaults/config.js index eeb02ea5..f26bb650 100644 --- a/defaults/config.js +++ b/defaults/config.js @@ -371,15 +371,22 @@ module.exports = { // 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 (&(=) ) + // The search query takes a couple of parameters in `searchDN`: + // - a base DN `searchDN/base`. Only children nodes of this DN will be likely + // to be returned; + // - a search scope `searchDN/scope` (see LDAP documentation); + // - the query itself, build as (&(=) ) // where is the user name provided in the log in request, // is provided by the config and is a filtering complement // also given in the config, to filter for instance only for nodes of type // 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: + // ,= + // where is the user name provided in the log in request, and + // and are provided by the config. + // ldap: { // // Enable LDAP user authentication @@ -399,33 +406,23 @@ module.exports = { // // 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 {} // // Example: // You can use this option in order to force the use of IPv6: // { - // host: 'my::ip::v6' - // servername: 'ldaps://example.com' + // host: 'my::ip::v6', + // servername: '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. + // LDAP base dn, alternative to searchDN // // @type string // - rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com", - - // - // Password of the lounge LDAP system user - // - // @type string - // - rootPassword: "1234", + // baseDN: "ou=accounts,dc=example,dc=com", // // LDAP primary key @@ -436,27 +433,55 @@ module.exports = { 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 - // @default "uid" + // @type object // - filter: "(objectClass=inetOrgPerson)(memberOf=ou=accounts,dc=example,dc=com)", + searchDN: { - // - // LDAP search base (search only within this node) - // - // @type string - // - base: "dc=example,dc=com", + // + // 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 + // + rootDN: "cn=thelounge,ou=system-users,dc=example,dc=com", - // - // LDAP search scope - // - // @type string - // @default "sub" - // - scope: "sub" + // + // Password of the lounge LDAP system user + // + // @type string + // + rootPassword: "1234", + + // + // 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 diff --git a/src/plugins/auth/_ldapCommon.js b/src/plugins/auth/_ldapCommon.js new file mode 100644 index 00000000..77a0962b --- /dev/null +++ b/src/plugins/auth/_ldapCommon.js @@ -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; + diff --git a/src/plugins/auth/advancedLdap.js b/src/plugins/auth/advancedLdap.js new file mode 100644 index 00000000..6d128e68 --- /dev/null +++ b/src/plugins/auth/advancedLdap.js @@ -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; diff --git a/src/plugins/auth/ldap.js b/src/plugins/auth/ldap.js new file mode 100644 index 00000000..3f81bf61 --- /dev/null +++ b/src/plugins/auth/ldap.js @@ -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; diff --git a/src/plugins/auth/local.js b/src/plugins/auth/local.js new file mode 100644 index 00000000..ebb8b137 --- /dev/null +++ b/src/plugins/auth/local.js @@ -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; diff --git a/src/server.js b/src/server.js index 205801cc..baefd7e5 100644 --- a/src/server.js +++ b/src/server.js @@ -11,7 +11,9 @@ var path = require("path"); var io = require("socket.io"); var dns = require("dns"); 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"); const net = require("net"); 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) { const socket = this; let client; @@ -538,11 +437,17 @@ function performAuthentication(data) { } // Perform password checking + let auth = function() {}; 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 { - localAuth(client, data.user, data.password, authCallback); + auth = localAuth; } + auth(manager, client, data.user, data.password, authCallback); } function reverseDnsLookup(ip, callback) {