Future = Npm.require('fibers/future'); var slug = function (text) { text = slugify(text, '.'); return text.replace(/[^0-9a-z-_.]/g, ''); }; // At a minimum, set up LDAP_DEFAULTS.url and .dn according to // your needs. url should appear as "ldap://your.url.here" // dn should appear in normal ldap format of comma separated attribute=value // e.g. "uid=someuser,cn=users,dc=somevalue" LDAP_DEFAULTS = { url: false, TLS: false, port: '389', dn: false, createNewUser: true, defaultDomain: false, searchResultsProfileMap: false, bindSearch: undefined }; /** @class LDAP @constructor */ var LDAP = function(options) { // Set options this.options = _.defaults(options, LDAP_DEFAULTS); // Make sure options have been set try { check(this.options.url, String); check(this.options.dn, String); } catch (e) { throw new Meteor.Error("Bad Defaults", "Options not set. Make sure to set LDAP_DEFAULTS.url and LDAP_DEFAULTS.dn!"); } // Because NPM ldapjs module has some binary builds, // We had to create a wraper package for it and build for // certain architectures. The package typ:ldap-js exports // "MeteorWrapperLdapjs" which is a wrapper for the npm module this.ldapjs = MeteorWrapperLdapjs; }; function startTLS(client) { var opts = { rejectUnauthorized: LDAP_DEFAULTS.rejectUnauthorized }; if ( LDAP_DEFAULTS.CACert && LDAP_DEFAULTS.CACert != '' ){ opts.ca = [LDAP_DEFAULTS.CACert]; } var starttlsSync = Meteor.wrapAsync(client.starttls); var res = starttlsSync(opts , null); if (res) { console.log("StartTLS Result: " + res); } } /** * Attempt to bind (authenticate) ldap * and perform a dn search if specified * * @method ldapCheck * * @param {Object} options Object with username, ldapPass and overrides for LDAP_DEFAULTS object */ LDAP.prototype.ldapCheck = function(options) { var self = this; options = options || {}; if (!options.hasOwnProperty('username') || !options.hasOwnProperty('ldapPass')) { throw new Meteor.Error(403, "Missing LDAP Auth Parameter"); } var ldapAsyncFut = new Future(); // Create ldap client var fullUrl = self.options.url + ':' + self.options.port; var client = self.ldapjs.createClient({ url: fullUrl, reconnect: false }); if (LDAP_DEFAULTS.TLS == true) { startTLS(client); } client.on('error', function(e) { ldapAsyncFut.return({error: e}); }); client.on('connect', function(e) { var bindSync = Meteor.wrapAsync(client.bind.bind(client)); // Slide @xyz.whatever from username if it was passed in // and replace it with the domain specified in defaults var emailSliceIndex = options.username.indexOf('@'); var username; var domain = self.options.defaultDomain; // If user appended email domain, strip it out // And use the defaults.defaultDomain if set if (emailSliceIndex !== -1) { username = options.username.substring(0, emailSliceIndex); domain = domain || options.username.substring((emailSliceIndex + 1), options.username.length); } else { username = options.username; } var bind = function(dn) { dn = dn.replace(/#{username}/g, options.username); console.log('Attempt to bind', dn); //Attempt to bind to ldap server with provided info client.bind(dn, options.ldapPass, function(err) { try { if (err) { // Bind failure, return error console.log(err); throw new Meteor.Error(err.code, err.message); } else { // Bind auth successful // Create return object var retObject = { username: username, searchResults: null }; // Set email on return object retObject.email = domain ? username + '@' + domain : false; // Return search results if specified if (self.options.searchResultsProfileMap) { client.search(dn, {}, function(err, res) { res.on('searchEntry', function(entry) { // Add entry results to return object retObject.searchResults = entry.object; ldapAsyncFut.return(retObject); }); }); } // No search results specified, return username and email object else { ldapAsyncFut.return(retObject); } } } catch (e) { console.log(e); ldapAsyncFut.return({ error: e }); } }); }; if (LDAP_DEFAULTS.bindSearch && LDAP_DEFAULTS.bindSearch.trim() != '') { try { var bindSearch = LDAP_DEFAULTS.bindSearch.replace(/#{username}/g, options.username); var opts = JSON.parse(bindSearch); if (opts.userDN && opts.password) { try { console.log('Bind before search', opts.userDN, opts.password); bindSync(opts.userDN, opts.password); delete opts.userDN; delete opts.password; } catch(e) { console.log('LDAP: Error', e); return ldapAsyncFut.return({ error: e }); } } console.log('LDAP search dn', options.ldapOptions.dn); console.log('LDAP search options', opts); client.search(options.ldapOptions.dn, opts, function(err, res) { if (err) { console.log('LDAP: Search Error', err); return ldapAsyncFut.return({ error: err }); } var entryCount = 0; var dn = self.options.dn; res.on('searchEntry', function(entry) { entryCount++; dn = entry.object.dn; }); res.on('error', function(err) { console.log('LDAP: Search on Error', err); ldapAsyncFut.return({ error: err }); }); res.on('end', function(result) { if (entryCount === 1) { bind(dn); } else { console.log('LDAP: Search returned', entryCount, 'record(s)'); var err = new Error('User not Found'); ldapAsyncFut.return({ error: err }); } }); }); } catch (e) { console.log('LDAP: BindSearch Error', e); ldapAsyncFut.return({ error: e }); } } else { bind(self.options.dn); } }); return ldapAsyncFut.wait(); }; // Register login handler with Meteor // Here we create a new LDAP instance with options passed from // Meteor.loginWithLDAP on client side // @param {Object} loginRequest will consist of username, ldapPass, ldap, and ldapOptions Accounts.registerLoginHandler("ldap", function(loginRequest) { var self = this; // If "ldap" isn't set in loginRequest object, // then this isn't the proper handler (return undefined) if (!loginRequest.ldap) { return undefined; } // Instantiate LDAP with options var userOptions = loginRequest.ldapOptions || {}; if (RocketChat.settings.get('LDAP_Sync_User_Data')) { userOptions.searchResultsProfileMap = true; } // Don't allow overwriting url and port delete userOptions.url; delete userOptions.port; var ldapObj = new LDAP(userOptions); // Call ldapCheck and get response var ldapResponse = ldapObj.ldapCheck(loginRequest); if (ldapResponse.error) { console.log(ldapResponse.error); console.log('[LDAP] Falling back to standard account base'); if (typeof loginRequest.username === 'string') if (loginRequest.username.indexOf('@') === -1) loginRequest.username = {username: loginRequest.username}; else loginRequest.username = {email: loginRequest.username}; loginRequest = { user: loginRequest.username, password: { digest: SHA256(loginRequest.ldapPass), algorithm: "sha-256" } } return Accounts._runLoginHandlers(self, loginRequest); // throw new Meteor.Error("LDAP-login-error", ldapResponse.error); } else { // Set initial userId and token vals var userId = null; var stampedToken = { token: null }; var username = slug(ldapResponse.username); // Look to see if user already exists var user = Meteor.users.findOne({ username: username }); // Login user if they exist if (user) { if (user.ldap !== true) { throw new Meteor.Error("LDAP-login-error", "LDAP Authentication succeded, but there's already an existing user with provided username ["+username+"] in Mongo."); } userId = user._id; // Create hashed token so user stays logged in stampedToken = Accounts._generateStampedLoginToken(); var hashStampedToken = Accounts._hashStampedToken(stampedToken); // Update the user's token in mongo Meteor.users.update(userId, { $push: { 'services.resume.loginTokens': hashStampedToken } }); } // Otherwise create user if option is set else if (ldapObj.options.createNewUser) { var userObject = { username: slug(ldapResponse.username) }; // Set email if (ldapResponse.email) userObject.email = ldapResponse.email; // Set profile values if specified in searchResultsProfileMap if (ldapResponse.searchResults && ldapObj.options.searchResultsProfileMap.length > 0) { var profileMap = ldapObj.options.searchResultsProfileMap; var profileObject = {}; // Loop through profileMap and set values on profile object for (var i = 0; i < profileMap.length; i++) { var resultKey = profileMap[i].resultKey; // If our search results have the specified property, set the profile property to its value if (ldapResponse.searchResults.hasOwnProperty(resultKey)) { profileObject[profileMap[i].profileProperty] = ldapResponse.searchResults[resultKey]; } } // Set userObject profile userObject.profile = profileObject; } userId = Accounts.createUser(userObject); Meteor.users.update(userId, {$set: { ldap: true }}); Meteor.runAsUser(userId, function() { Meteor.call('joinDefaultChannels'); }); } else { // Ldap success, but no user created throw new Meteor.Error("LDAP-login-error", "LDAP Authentication succeded, but no user exists in Mongo. Either create a user for this email or set LDAP_DEFAULTS.createNewUser to true"); } // LDAP sync data logic var syncUserData = RocketChat.settings.get('LDAP_Sync_User_Data'); var syncUserDataFieldMap = RocketChat.settings.get('LDAP_Sync_User_Data_FieldMap').trim(); if (userId && syncUserData && syncUserDataFieldMap) { var userData = {}; var fieldMap = JSON.parse(syncUserDataFieldMap); var emailList = []; _.map(fieldMap, function(userField, ldapField) { if (!ldapResponse.searchResults.hasOwnProperty(ldapField)) { return; } // restrict field mapping to a known list of fields switch (userField) { case 'email': if ('object' == typeof ldapResponse.searchResults[ldapField]) { _.map(ldapResponse.searchResults[ldapField], function (item) { emailList.push({ address: item, verified: true }); }); } else { emailList.push({ address: ldapResponse.searchResults[ldapField], verified: true }); } break; case 'name': userData.name = ldapResponse.searchResults[ldapField]; break; default: break; } }); if (emailList.length) { userData.emails = emailList; } if (_.size(userData)) { Meteor.users.update(userId, { $set: userData }); } } return { userId: userId, token: stampedToken.token }; } return undefined; });