ldap-bridge
remotely backed LDAP authentication
License
MIT
About
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.
Example
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.
Security
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.
Setup
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:
npx ldap-bridge
Docker Support
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 data
docker 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
Configuration
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.
Backend Definition
In section 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 pop3
and 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 john.doe@foo.com
is trying to bind, john.doe
is 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.
Client Filtering
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.
Usage
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 "(uid=john.doe@foo.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
Supported Queries
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.
Searching Entry
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.
(uid=john.doe@foo.com)
(&!(objectclass=group)(level>3)(mailPublic=john.doe@foo.com))
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=search
to 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 dc
before concatenating all resulting RDNs. E.g, john.doe@foo.com
has domain foo.com
which is resulting in DN dc=foo,dc=com
.
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.
Binding
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.
Logging
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 *:alter,*:error,*:warn,*:info
.
Encryption Required
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.
Attribute Qualification
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.surname@domain.tld
givenName.surname_objectClass@domain.tld
givenName.anotherGivenName.surname@domain.tld
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
ann-mary.jane.miller-smith_admin@foo.com
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
Fuzzy Queries
Sub-Addressing in LDAP Searches
When searching for a user by mail address its sub-addressing part is ignored. So, searching for
john.doe+office@foo.com
results in
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:
{
"mailbox": "john.doe",
"domain": "foo.com",
"address": "john.doe@foo.com"
}