remotely backed LDAP authentication
This package implements an LDAP service suitable for authenticating users. It is translating incoming requests for binding as a user into authentication requests against configurable backend services such as POP3.
Using this service, you can set up an LDAP-aware service such as Jitsi or Mattermost for users authenticating against the POP3 of your local MTA.
In production mode the service requires encrypted connections with its clients as well as with backend services. By intention, it is forwarding any password provided by a client to related backend service. It isn't storing passwords. It isn't caching any result. The service is working stateless.
As a beneficial side effect this makes the service horizontally scalable.
From a user's point of view this tool implies a severe security issue as soon as it's used to authenticate against remote backends you don't manage. This is due to the possibility of reading/tracking sensitive information meant to be shared by your users and any such remote backend, only.
Install the package globally:
npm install -g @cepharum/ldap-bridge
Create a configuration file config.js in current working directory with content from distributed template file. Adjust it according to your needs.
For production setup you need to provide a certificate and its private key as files in PEM format. You can obtain a certificate from LetsEncrypt using Certbot. See that tool's documentation for additional information.
Eventually start the service with:
The latest version is available as a docker image named registry.gitlab.com/cepharum-foss/ldap-bridge, too.
On first run this image is writing configuration template into mounted volume for review.
mkdir -p datadocker run -it --rm -v $(pwd)/data:/config registry.gitlab.com/cepharum-foss/ldap-bridge
Adjust the configuration in data/config.js according to your needs.
For production setup you need to provide a certificate and its private key as files in PEM format. Put those files in same folder data and use relative filenames in your configuration. A certificate can be obtained from LetsEncrypt using Certbot. See that tool's documentation for additional information.
Now you can start the container with:
docker run -d --rm -p 636:636 -v $(pwd)/data:/config registry.gitlab.com/cepharum-foss/ldap-bridge
All configuration is available and roughly documented in config.js file.
Configuration consists of these parts:
- LDAP service configuration
- backend definition
- common client filter list
LDAP Service Configuration
The LDAP service is customized in section
server of configuration. It is selecting IP and port of listener socket as well as the TLS certificate and key to use. The TLS file names may be provided relatively to the used config.js file.
backends of configuration backend domains are mapped into backend selectors.
The backend domain is matched against the domain part of a mail address looked up and used by an LDAP client for authentication to pick the backend for processing either request.
The backend selector is a string resembling a URL and describing the backend to use as well as options for customizing it.
The URL's scheme is required. It picks the backend implementation by type. Supported schemes are
pop3s. Both select the same POP3 backend which is requiring encrypted POP3 connections.
Following the scheme a hostname and a port may be given to address the remote POP3 service to communicate with. This must be given when looking up the backend domain wouldn't address the POP3 server to use.
Preceding the remote service's hostname, there may be a
:-separated list of options separated by a single
@ from the hostname. The order of options doesn't matter. There are two kinds of supported options:
Options containing at least one period are considered domain names of clients collected as backend-specific entries of client filter (see below).
All other options are assumed to name boolean switches. If provided, the related switch is set. In addition either switch may be preceded by a
+to set the switch more explicitly or a
-to clear it. POP3 backend is supporting following switches:
short: By setting this switch the local part of mail address provided as username is forwarded to the POP3 service for authentication, only.
E.g., when user
firstname.lastname@example.org trying to bind,
john.doeis used as username when trying to authenticate at remote POP3 service, only. By default, this switch is cleared, thus the whole mail address is forwarded.
The third part of configuration is listing domain names of LDAP clients permitted to request, only. In addition, for every backend specific clients may be listed. If the resulting list per backend is empty, any client's request is accepted. Otherwise, the requesting client's IP is looked up in DNS for a PTR RR providing the client's hostname. This hostname must be listed here for accepting that client's request.
This feature is important to improve the tool's security.
Assuming you've set up ldap-bridge to be available at ldaps://ldap.foo.com, you can use regular client libraries and tools for LDAP to query the service and for binding as a particular user:
ldapsearch -x -H ldaps://ldap.foo.com -b cn=search "(email@example.com)"
This might result in a response returning an entry similar to this one:
dn: uid=john.doe,dc=foo,dc=com objectclass: top objectclass: user uid: john.doe
The service is limited to handling queries basically required to authenticate a user against an LDAP directory:
A client searches the LDAP directory for a single entry satisfying a filter which is testing a single attribute for matching a user's login name. On success, that entry has a unique DN.
The found entry's DN is used in combination with a user-provided password to bind as that DN which is LDAP speak for authenticating as that user.
The LDAP service expects searching queries to include a filter which is looking for an entry matching a given attribute by value. This is called an equality filter. A search filter might be simple or complex.
Both examples are valid LDAP filters. The service doesn't have access on actual user data, so it can't process them properly, though. Instead, it is extracting all contained positive equality filters addressing any of the attributes uid, user, username, login, loginname, mail or mailpublic. Their values are collected as usernames.
This flexibility enables support for most LDAP clients that usually build complex filters irregardless of their configuration.
The extracted number of usernames is essential. On extracting a single username it is considered a match candidate. For convenience, the bind DN is searched implicitly on binding requests when no username has been extracted from search filter before. In every other case the service instantly responds with an empty result set.
The service doesn't handle a query's scope. However, the base DN is important:
Use special base DN
cn=searchto automatically pick the backend matching the extracted username. The username must be formatted like a mail address. The domain part is used to pick the related backend.
For every configured backend a DN is derived from its domain which becomes available as a base DN, too. Any search request using this base DN is limited to that backend.
On deriving the DN, every segment of backend's domain is converted into an RDN addressing that segment in attribute
dcbefore concatenating all resulting RDNs. E.g,
foo.comwhich is resulting in DN
Search requests with such a base DN may omit the domain part in searched user's name for convenience. The backend's domain is used as fallback in those cases.ldapsearch -x -H ldaps://ldap.foo.com -b dc=foo,dc=com "(uid=john.doe)"
When a backend is found matching for the domain of provided user's mail address, a virtual LDAP entry is returned to describe this user.
A user's DN as returned from search queries must be used for binding.
ldapsearch -x -H ldaps://ldap.foo.com -D uid=john.die,dc=foo,dc=com -b dc=foo,dc=com -W
After providing your password you'll see the authenticated user's entry again.
The LDAP service is using the bind DN for picking the backend to use. In addition, the bind DN is used to extract the user's mail address which is forwarded by the backend to a remote service for authentication. On success the implied search is processed. As a fallback, the bound user's DN is searched.
The service is logging to the console using debug. Thus, you can use DEBUG environment variable to adjust log levels. In docker image this defaults to
In a production setup the service requires LDAP and backends to communicate over encrypted connections, only.
For development purposes you should set NODE_ENV environment variable to
development to work with non-encrypted LDAP server locally.
When delivering LDAP entries as results of a search query those results may be qualified depending on a found user's mail address. It supports the following mail address formats resulting in additional LDAP attribute values for givenName, sn (for surname) and objectclass returned:
givenName.firstname.lastname@example.org givenName.surname_objectClass@domain.tld givenName.anotherGivenName.email@example.com givenName.anotherGivenName.surname_objectClass@domain.tld
Either given name and the surname are converted to have leading capitals. Dashes are supported in those parts of address as well. Multiple given names separated by period in mail address are provided space-separated in resulting givenName attribute.
For example, the mail address
results in LDAP entry
dn: uid=ann-mary.jane.miller-smith_admin,dc=foo,dc=com objectclass: top objectclass: user objectclass: admin uid: ann-mary.jane.miller-smith_admin givenName: Ann-Mary Jane sn: Miller-Smith
Sub-Addressing in LDAP Searches
When searching for a user by mail address its sub-addressing part is ignored. So, searching for
dn: uid=john.doe,dc=foo,dc=com objectclass: top objectclass: user uid: john.doe givenName: John sn: Doe
The +office sub-address is omitted.
Additional RDNs in Bind DN
Any additional RDN is ignored on binding as a given user. The following bind DNs result in identical authentication requests against some matching backend:
uid=john.doe,dc=foo,dc=com uid=john.doe,ou=people,dc=foo,dc=com uid=john.doe,ou=staff,ou=people,dc=foo,dc=com
In either case the assumed user is described as: