{"id":16994965,"url":"https://github.com/jackadams/meteor-accounts-ldap","last_synced_at":"2025-03-23T17:31:13.583Z","repository":{"id":33433409,"uuid":"37078721","full_name":"JackAdams/meteor-accounts-ldap","owner":"JackAdams","description":"Authentication against an LDAP server in Meteor","archived":false,"fork":false,"pushed_at":"2022-11-22T07:59:04.000Z","size":124,"stargazers_count":21,"open_issues_count":6,"forks_count":13,"subscribers_count":5,"default_branch":"master","last_synced_at":"2025-03-18T22:26:19.708Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/JackAdams.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-06-08T16:27:11.000Z","updated_at":"2024-03-06T04:09:07.000Z","dependencies_parsed_at":"2023-01-15T01:00:46.952Z","dependency_job_id":null,"html_url":"https://github.com/JackAdams/meteor-accounts-ldap","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JackAdams%2Fmeteor-accounts-ldap","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JackAdams%2Fmeteor-accounts-ldap/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JackAdams%2Fmeteor-accounts-ldap/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/JackAdams%2Fmeteor-accounts-ldap/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/JackAdams","download_url":"https://codeload.github.com/JackAdams/meteor-accounts-ldap/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245140930,"owners_count":20567475,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T03:47:12.062Z","updated_at":"2025-03-23T17:31:13.040Z","avatar_url":"https://github.com/JackAdams.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"Accounts LDAP\n-------------\n\n__BREAKING CHANGE: v0.7.0 (uses ldapjs v1.0.0 instead of v0.7.1) is published without platform-specific builds, so a [build toolchain](https://guide.meteor.com/1.4-migration.html#binary-packages-require-build-toolchain) will need to be installed on each target deployment machine.__ If you are having issues, you can also try installing the bcrypt package from meteor: `meteor add npm-bcrypt`.\n\nAuthentication and account management for Meteor using LDAP.\n\n#### Overview\n\nThis is a package to implement authentication against a separate directory server via LDAP and retrieval of user attributes from that server. It is an adaptation of `hive:accounts-ldap`.\n\nThe things that this package does differently from `hive:accounts-ldap` are:\n\n- UI matches that of the core package `accounts-ui`\n- the UI can be replaced by adding another package\n- user data (email address, etc.) is stored in the same format as that of the core package `accounts-password`\n- users can authenticate using either username or email address\n- LDAP settings can be set programatically or using a `settings.json` file\n- this package can peacefully co-exist with the core `accounts-password` package\n- there are hooks and methods that can be used for multi-tenant apps that store ldap connection info in collections\n- there is more flexibility around the LDAP bind and search methods, allowing it to work with a range of different directory configurations\n\n#### Installation\n\n`meteor add babrahams:accounts-ldap`\n\n#### Usage\n\nYour server's URL and a DN or DNs to search will need to be set in a settings.json file as `serverUrl` and `serverDn`, respectively. In addition, you can select an array of `whiteListedFields` from an LDAP search to add to the `user.profile` field in the document created in `Meteor.users`. Or (optionally) choose a globally unique field to be recorded in the app database for more reliable lookups on subsequent logins. An example for the settings.json file is:\n\n```\n{\n  \"ldap\": {\n    \"serverDn\": \"DC=ad,DC=university,DC=edu\",\n    \"serverUrl\": \"ldap://ad.university.edu:389\",\n    // \"whiteListedFields\": [ \"displayName\", \"givenName\", \"department\", \"employeeNumber\", \"mail\", \"title\", \"address\", \"phone\", \"memberOf\"],\n    // \"autopublishFields\": [ \"displayName\", \"department\", \"mail\", \"title\", \"address\", \"phone\"],\n\t// \"uniqueIdentifier\": \"objectGUID\"\n  }\n}\n```\n\n**OR**\n\nTo create settings programatically, overwrite the function below somewhere in your server code\n\n```\n// Overwrite this function to produce settings based on the incoming request\nLDAP.generateSettings = function (request) {\n  return null;\t\n}\n```\n\nreturning an object of the form:\n\n```\n{\n  \"serverDn\": \"DC=ad,DC=university,DC=edu\",\n  \"serverUrl\": \"ldap://ad.university.edu:389\",\n  \"whiteListedFields\": [ \"displayName\", \"givenName\", \"department\", \"employeeNumber\", \"mail\", \"title\", \"address\", \"phone\", \"memberOf\"],\n  \"autopublishFields\": [ \"displayName\", \"department\", \"mail\", \"title\", \"address\", \"phone\"]\n}\n```\n\n#### API\n\n##### Client\n\nYou can send info from the client to your app server via the request parameter by overwriting the below function **on the client** whose return value will set the value of `request.data`:\n\n```\nLDAP.data = function () { return null; };\n```\n\nYou can set a username to display for the logged in user by overwriting the following function:\n\n```\nLDAP.username = function (user) { return ''; };\n```\nIf you don't overwrite it, `currentUser.username` will be used; or, if that isn't found, the first email address for that user; or, if that isn't found, \"Authenticated user\".\n\nThe is a client-side hook that can be used when a user logs in successfully:\n\n```\nLDAP.onSuccessfulLogin = function (user) { };\n```\nOverwrite this function with whatever you need to happen after a user logs in.\n\nThe following are only used if you want to create a custom sign-in form:\n\n```\nLDAP.customFormTemplate.set(\"customFormTemplateName\");\n```\n\n`LDAP.formHelpers` and `LDAP.formEvents` are the helpers and events hashes that the original form uses and are available to custom forms.\n\nA full working implementation of a custom form is here:\n\n[babrahams:accounts-ldap-ionic](https://atmospherejs.com/babrahams/accounts-ldap-ionic) ([github repo](https://github.com/JackAdams/meteor-accounts-ldap-ionic))\n\n##### Server\n\nYou can produce a custom bind value (value that is used with the user-submitted password to bind to the directory server via LDAP) by overwriting this function **on the server**:\n\n```\nLDAP.bindValue = function (usernameOrEmail, isEmailAddress, FQDN) {\n  return ((isEmailAddress) ? usernameOrEmail.split('@')[0] : usernameOrEmail) + '@' + FQDN;\t\n}\n```\n\nYou can create a custom search filter by overwriting the `LDAP.filter` function **on the server** (if the default version, shown below, does not work for your particular LDAP configuration):\n\n```\n// This filter, used with default settings for LDAP.searchField assumes that the part of the email address before the @ perfectly matches the cn value for each user\n// Overwrite this if you need a custom filter for your particular directory server configuration\n// For example if everyone has the 'mail' field set, but the bit before the @ in the email address doesn't exactly match users' cn values, you could do:\n// LDAP.filter = function (isEmailAddress, usernameOrEmail, FQDN) { return '(\u0026(' + ((isEmailAddress) ? 'mail' : 'cn') + '=' + usernameOrEmail + ')(objectClass=user))'; }\n\nLDAP.filter = function (isEmailAddress, usernameOrEmail, FQDN) {\n  var searchField = (_.isFunction(LDAP.searchField)) ? LDAP.searchField.call(this) : LDAP.searchField;\n  var searchValue = LDAP.searchValue.call(this, isEmailAddress, usernameOrEmail, FQDN);\n  return '(\u0026(' + searchField + '=' + searchValue + ')(objectClass=user))';\n}\n\nLDAP.searchValue = function (isEmailAddress, usernameOrEmail, FQDN) {\n  var username = (isEmailAddress) ? usernameOrEmail.split('@')[0] : usernameOrEmail;\n  var searchValue;\n  var searchValueType = (_.isFunction(LDAP.searchValueType)) ? LDAP.searchValueType.call(this) : LDAP.searchValueType;\n  switch (searchValueType) {\n\tcase 'userPrincipalName' :\n\t  searchValue = username + '@' + FQDN;\n\t  break;\n\tcase 'email' :\n\t  searchValue = (isEmailAddress) ? userNameOrEmail : username + '@' + FQDN; // If it's not an email address, we're kind of guessing\n\t  break;\n\tcase 'username' :\n\tdefault :\n\t  searchValue = username;\n  }\n  return searchValue;\n}\n```\n\nIf you don't need to overwrite the whole filter, you can just set the following **on the server** (for example in AD with the `userPrincipalName` values in the standard format for each user):\n\n```\nLDAP.searchField = 'userPrincipalName';\nLDAP.searchValueType = 'userPrincipalName';\n```\n\nThe default settings are:\n\n```\nLDAP.searchField = 'cn';\nLDAP.searchValueType = 'username';\n```\n\nYou can set `LDAP.searchField` and `LDAP.searchValueType` to be string values or functions that return string values.\n\nThe three `LDAP.searchValueType` values that are built in are: `username`, `email`, and `userPrincipalName`.\n\n```\nLDAP.attributes = [];\n```\nSet this value **on the server** to explicitly specify what attributes you want to return from the LDAP server.\n```\nLDAP.attributes = ['uid', 'cn', 'memberOf'];\n```\n\nYou can use this to return only the LDAP attributes that you're interested in, or to request attributes that\n are *not* returned by default, like OpenLDAP *operational attributes*.\n\nAn empty array (the default) will return the default attributes.\n\n\n```\nLDAP.tryDBFirst = true;\n```\n**on the server** if you want the package to try and log the user in using the app database before hitting the LDAP server. (This is `false` by default.)\n\n```\n// This is used to produce the query that checks whether there is an existing user in the database when LDAP.tryDBFirst = true\n// It can be overwritten to produce custom selectors\nLDAP.userLookupQuery = function (fieldName, fieldValue, isEmail, isMultitenantIdentifier) {\n  // Context (this) is the request sent from client\n  var selector = {};\n  selector[fieldName] = fieldValue;\n  // Must return a mongo selector -- e.g. {username: \"jackadams\"} or {\"email.address\": \"example@example.com\"}\n  return selector;\n}\n```\n\n```\nLDAP.logging = false;\n```\n**on the server** if you want to suppress output to the server console (this is `true` by default, to help with debugging during the initial setup phase)\n\nYou can optionally overwrite the following logging function **on the server** to manage logging yourself:\n\n```\nLDAP.log: function (message) {\n  if (LDAP.logging) {\n\tconsole.log(message);\n  }\n}\n```\n\nAdditionally you can overwrite the following warn and error functions **on the server** to handle warnings and errors \nseparately (per default they both log their messages on the console if logging is enabled):\n\n```\nLDAP.warn: function (message) {\n  this.log(message);\n},\nLDAP.error: function (message) {\n  this.log(message);\n}\n```\n\nThis is a hook you can use **on the server** when a user successfully signs in using LDAP (doesn't fire if the sign in is via the app database when using `LDAP.tryDBFirst`)\n```\nLDAP.onSignIn(function (userDocument, userData, ldapEntry) {\n  // Do things to user document like Roles.removeUsersFromRoles(userDocument, 'admin')\n});\n```\nThe purpose of this hook is to let the app modify a user document if it finds conditions have changed on the directory server (e.g. the user is no longer an admin or has left the organization) and it needs to mirror this in its own db document(s). `this` in the function is the sign in request (an object) sent from the client (which contains the plain text password, as does the `userData` parameter). `userDoc`, `userData`, and `ldapEntry` are all js objects.\n\nOverwrite this function **on the server** to modify the condition used to find an existing user:\n\n```\nLDAP.modifyCondition = function (condition, userObj) {\n  // `this` is the request from the client\n  // `userObj` has the fields `email`, `username`, `password` (plain text), `profile`\n  return condition;    \n}\n```\nThe condition passed to this function is of the form:\n```\n{emails: {$elemMatch: {address: \u003cemailAddress\u003e}}};\n```\nif an email address was typed in the login form on the client, or, if a username was typed in the login form on the client, it is of this form:\n```\n{username: \u003cusername\u003e}\n```\n\nA similar search condition should be returned by the function. `this` in this function's context is the request object received from the client.\n\n**Note:** if there is a possibility that your Meteor app allows duplicate usernames or email addresses, you could overwrite this function. But, better than this, is to use `LDAP.multitenantIdentifier` as shown below.\n\nTo make sure a multi-tenant app doesn't get mixed up with duplicate usernames or passwords, set:\n\n```\nLDAP.multitenantIdentifier = 'tenant_id';\n```\nwhere `'tenant_id'` is a string that gives the name of a key from `request.data`, as sent from the client using `LDAP.data` (see above). The value associated with this key must be a unique id value for the tenant.\n\n**Note:** if you use `LDAP.multitenantIdentifier`, then `LDAP.modifyCondition` will have no effect, as the package will create the user identifier for you. Also, a new field `ldapIdentifier` will be added to each document added to the `Meteor.users` collection by this package.\n\n```\nLDAP.appUsername = function (userNameOrEmail, isEmail, userObj) {\n  // userObj contains the best guess we've got for email and username, one of which successfully retrieved a user from the directory using LDAP\n  // `this` is the request received from the client\n  return (isEmail) ? userNameOrEmail.split('@')[0] : userNameOrEmail;\t\n}\n```\n\nOverwrite this function **on the server** if the app needs to do something to modify the username of the users in the app database (i.e. the username field in the app should be different from the username field in the directory accessed via LDAP).  Mainly for multi-tenant apps where users belonging to different tenants can have the same username, but because there is a unique index on the `username` field in the `users` collection, this can't happen without modifying the username when: 1) creating a user, 2) doing lookups by username. Overwriting this function takes care of both of those, provided you guarantee unique usernames.\n\nFull example:\n\n_Client_\n```\nLDAP.data = function () {\n  return {\n    tenant_id: Session.get('tenant_id')\n  };\n};\n```\n_Server_\n```\nLDAP.multitenantIdentifier = 'tenant_id';\n```\n\nOverwrite the function below **on the server** to add custom fields to the new user document created when a user from the directory server isn't found in the Meteor app's database (based on the 'condition' discussed above):\n```\nLDAP.addFields = function (person) {\n  // `this` is the request from the client\n  // `person` is the object returned from the LDAP server\n  // return the fields that are to be added when creating a user as an object with {key: value} pairs\n  return {};\t\n}\n```\n\nThe following hook can be used **on the server**:\n```\nLDAP.onAddMultitenantIdentifier(function (ldapIdentifier, userDocument, userData) {\n  // Do things to user document like Roles.setUserRoles(userDocument, 'admin', 'foo-organization')\n});\n```\nThe reason for this hook's existence is that when an existing user sucsessfully signs into a different tenant in an app that has `LDAP.multitenantIdentifier` set, this package will add a new value to the array in the `ldapIdentifier` field, but it won't deal with any of the extra fields created using `LDAP.addFields` (as these may have only been for new account creation).  However, if the app needs to update the user document due to the fact that this is the first time a user has signed in to this particular tenant (e.g. to add roles in this tenant's context), then this hook is available.\n\n**Note:** the `userData` parameter contains the plain text password, so be careful what you do with the userData object.  For instance, don't stringify and log it somewhere insecure!\n\n#### Built in UI\n\n`{{\u003e ldapLoginButtons}}` renders a template with username/email and password inputs. If login is successful, the user will be added to the `Meteor.users` collection. It is up to the app to publish and subscribe to certain fields from the user document. By default, only the username is published.\n\n#### Warning\n\nPassword is sent from client to server in plain text.  Only use this package in conjunction with SSL.\n\nAlthough this package supports multi-tenancy, where each tenant has their own directory server, the connection between Meteor app server and directory server is unencrypted (waiting on a new version of `ldapjs` that supports TLS) unless the directory server is using ssl - i.e. `ldaps://`. Because plain text passwords are sent from the app to the directory server, you really shouldn't use this package in any app that sits outside the corporate firewall!\n\n#### TODO\n\n- make the sign in form more configurable with options like:\n  - `unstyled=true` - to remove all classes\n  - `alwaysOpen` - to make the form automatically open\n  - `loggedOutLinkTemplate` - to replace the default link that you click to open the form\n  - `loggedInLinkTemplate` - to replace the default link that you click to get the dropdown once logged in\n- work on securing traffic from client through to LDAP server\n- automated testing\n  \n#### Example code\n\n_From a working multi-tenant app (where 'tenants' are called 'organizations')._\n\n###### Client\n\n```\nLDAP.data = function () {\n  return {organization_id: App.state.get('organization_id') || null};  \n}\n```\n\n###### Server\n\n```\nLDAP.logging = false;\nLDAP.tryDBFirst = true;\n\nLDAP.generateSettings = function (request) {\n  if (request.data \u0026\u0026 request.data.organization_id) {\n\tvar organization_id = request.data.organization_id;\n\tcheck(organization_id, String);\n\tvar organization = Organizations.findOne({_id: organization_id});\n\tif (organization.ldap) {\n\t  var ldapSettings = organization.ldap;\n\t  return {\n\t\t\"serverDn\": ldapSettings.serverDn,\n\t\t\"serverUrl\": ldapSettings.serverUrl,\n\t\t\"whiteListedFields\": [ldapSettings.displayNameField || \"displayName\"],\n\t\t\"autopublishFields\": []\n\t  };\n\t}\n  }\n  return null;  \n}\n\nLDAP.multitenantIdentifier = 'organization_id';\n\nLDAP.onAddMultitenantIdentifier(function (addedIdentifier, userDoc, userData) {\n  // `this` is the request object if we need it\n  // currentOrganization needs to be set and so do roles\n  var updates = {};\n  if (userData \u0026\u0026 userData.currentOrganization) {\n\t// The relevant userData was set by the LDAP.addFields function below\n\t// But the user already existed, so roles and \n\tMeteor.users.update({_id: userDoc._id}, {$set: {currentOrganization: userData.currentOrganization}}); \n\tif (userData \u0026\u0026 userData.roles \u0026\u0026 userData.roles[userData.currentOrganization]) {\n\t  Roles.addUsersToRoles(userDoc._id, userData.roles[userData.currentOrganization], userData.currentOrganization);\n\t}\n  }\n});\n\nLDAP.addFields = function (person) {\n  var newUserFields = {};\n  var organization_id = this.data \u0026\u0026 this.data.organization_id;\n  if (organization_id) {\n    newUserFields.currentOrganization = organization_id;\n\tif (userIsLeader(person, organization_id)) {\n\t  // This is sidestepping the roles package Roles.addUsersToRole function, but has the same effect\n\t  // The reason for this is, the user doesn't exist yet! (So we can't add roles to them.)\n\t  var roles = {};\n\t  roles[organization_id] = [\"leader\"];\n\t  newUserFields.roles = roles;\n\t}\n  }\n  return newUserFields;\n}\n\nLDAP.onSignIn(function (user, userData, ldapEntry) {\n  // `this` is the request sent\n  // Check whether the user is still a leader or has become a leader and act accordingly\n  // Not much point to this, while LDAP.tryDBFirst = true; as onSignIn only fires when the LDAP server is hit\n  // And it should never get hit again in subsequent sign ons unless the user changes organizations\n  var person = ldapEntry;\n  var isReallyLeader = userIsLeader(person, user.currentOrganization);\n  var isLeaderinApp = Roles.userIsInRole(user, 'leader', user.currentOrganization);\n  // If the person has stopped being a leader\n  if (isLeaderinApp \u0026\u0026 !isReallyLeader) {\n    Roles.removeUsersFromRoles(user, 'leader', user.currentOrganization);\n  }\n  if (!isLeaderinApp \u0026\u0026 isReallyLeader) {\n\tRoles.setUserRoles(user, 'leader', user.currentOrganization);  \n  }\n});\n\n// LDAP specific function, which is why it's in this file\nvar userIsLeader = function (person, organization_id) {\n  var leader = false;\n  // Need to parse the LDAP object and determine whether this person is a leader in the organization\n  if (organization_id) {\n\tvar organization = Organizations.findOne({_id: organization_id});\n\tif (person.dn \u0026\u0026 organization \u0026\u0026 organization.ldap \u0026\u0026 organization.ldap.leaderDnContains \u0026\u0026 _.isArray(organization.ldap.leaderDnContains)) {\n\t  var leader = _.reduce(organization.ldap.leaderDnContains, function (memo, searchString) {\n\t\tif (person.dn.indexOf(searchString) === -1) {\n\t\t  memo = false;\t\n\t\t}\n\t\treturn memo;\n\t  }, true);\n\t}\n  }\n  return leader;\n}\n```\n\nDocuments from the `Organizations` collection look like this:\n\n```\n{\n  \"_id\": \"45JE8Q6zufss7Fwzx\"\n  \"name\": \"My Organization\",\n  \"ldap\": {\n    \"serverDn\": \"OU=people,OU=we_like,DC=myorganization,DC=com\",\n    \"serverUrl\": \"ldap://pdc.myorganization.com:389\",\n    \"leaderDnContains\": [\n      \"Leader\",\n      \"Currently_Active\"\n    ]\n  }\n}\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackadams%2Fmeteor-accounts-ldap","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fjackadams%2Fmeteor-accounts-ldap","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fjackadams%2Fmeteor-accounts-ldap/lists"}