Contents
- Description
- Installation
- Configuration
- Examples
- Directory compatibility
- Multi-factor authentication
- Migrating from 0.x
- Development
- License
Description
An LDAP Authenticator plugin for JupyterHub, written with Enterprise LDAP and Active Directory integration in mind. It supports:
- Multiple LDAP servers with configurable high-availability pooling (
server_pool_strategy) - Search-bind authentication using a service account, or direct-bind via
bind_dn_template - Modern TLS negotiation (
server_tls_strategy:before_bind/on_connect/insecure) - Group-based access control with
allowed_groups, including recursive nested group resolution - Automatic creation of a user's home directory at login
- Exposing selected LDAP attributes to JupyterHub as
auth_state
Which LDAP authenticator should I use? The JupyterHub organization maintains an official
jupyterhub-ldapauthenticatorpackage. This project is a superset of it — reach for this plugin when you need any of its distinguishing features: automatic home-directory creation, multi-server failover pooling, or nested-group resolution. For simpler deployments, the official package is an excellent choice.
Installation
Install with pip:
pip install jupyterhub-ldap-authenticator
Configuration
To enable LDAPAuthenticator, add the following to your JupyterHub config file and
extend it with the parameters below.
c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator'
Authentication strategy
This plugin supports two authentication strategies:
- Search-bind (default): connect as a service account (
bind_user_dn/bind_user_password), search the directory for the authenticating user, then verify their password. Requiresbind_user_dn,bind_user_password,user_search_base, anduser_search_filter. - Direct-bind: bind to the server directly as the authenticating user using
bind_dn_template, with no service account. Enabled automatically whenbind_dn_templateis set.
Fully worked configurations for each strategy are in Examples.
Server parameters
| Parameter | Description | Default |
|---|---|---|
server_hosts |
List of names, IPs, or complete scheme://host:port URLs of the LDAP server(s). |
required |
server_port |
Port the LDAP server listens on. Typically 389 (cleartext) or 636 (secured). | None |
server_tls_strategy |
SSL/TLS strategy: before_bind (STARTTLS, recommended), on_connect (legacy LDAPS, port 636), or insecure (no TLS — cleartext). |
before_bind |
server_tls_kwargs |
Keyword arguments passed to the ldap3 Tls object. Ignored when server_tls_strategy='insecure'. |
{} |
server_use_ssl |
Deprecated since 1.0. True is equivalent to server_tls_strategy='on_connect'. Use server_tls_strategy instead. |
False |
server_connect_timeout |
Timeout, in seconds, when establishing a connection before raising an exception. | None |
server_receive_timeout |
Timeout, in seconds, for responses from an established connection before raising an exception. | None |
server_auto_referrals |
Whether ldap3 automatically follows server referrals. Disabled by default because chasing a referral re-sends the bind credentials to the referred server, which can leak them to a server chosen by the directory. Enable only if you require referral chasing and trust every server that may be referred. | False |
server_pool_strategy |
Pool HA strategy: FIRST, ROUND_ROBIN, or RANDOM. |
FIRST |
server_pool_active |
If True, check server availability. Set to an integer for the maximum number of cycles to try before giving up. |
True |
server_pool_exhaust |
If True, remove inactive servers from the pool. Set to an integer for the number of seconds an unreachable server is considered offline. |
False |
⚠️ Certificate validation. By default the ldap3
Tlsobject does not verify the server's certificate (validate=ssl.CERT_NONE), sobefore_bindandon_connectencrypt the connection but do not authenticate the server — leaving credentials exposed to man-in-the-middle attacks. When TLS is enabled without validation, the authenticator logs a warning at startup. For production, enable verification against a CA bundle viaserver_tls_kwargs:import ssl c.LDAPAuthenticator.server_tls_kwargs = { 'validate': ssl.CERT_REQUIRED, 'ca_certs_file': '/etc/ssl/certs/ca-bundle.pem', }
Bind parameters
| Parameter | Description | Default |
|---|---|---|
bind_dn_template |
Template(s) for the full DN used to direct-bind as the authenticating user, bypassing the service-account search. {username} is substituted with the username. Accepts a string or a list of templates (tried in order). When set, the direct-bind strategy is used. |
None |
bind_user_dn |
Service-account DN used for simple bind. Required for the search-bind strategy. | None |
bind_user_password |
Service-account password used for simple bind. Required for the search-bind strategy. | None |
# direct-bind: a single template, or a list tried in order until one binds
c.LDAPAuthenticator.bind_dn_template = 'uid={username},ou=people,dc=example,dc=org'
c.LDAPAuthenticator.bind_dn_template = [
'uid={username},ou=people,dc=example,dc=org',
'uid={username},ou=developers,dc=example,dc=org',
]
User and group parameters
| Parameter | Description | Default |
|---|---|---|
user_search_base |
Location in the DIT where the user search starts. | required (search-bind) |
user_search_filter |
LDAP filter validating that the user exists. {username} is substituted with the authenticating username. |
required (search-bind) |
user_membership_attribute |
LDAP attribute holding the user's group membership. | memberOf |
group_search_base |
Location in the DIT where the nested-group search starts. {group} is substituted with entries from allowed_groups. |
none |
group_search_filter |
LDAP filter returning members of groups in allowed_groups. {group} is substituted with the group DN. |
none |
allowed_groups |
List of group DNs a user must belong to in order to log in. If unset, group scoping is short-circuited and all authenticated users are allowed. | None |
admin_groups |
List of group DNs whose members are granted JupyterHub admin. Resolved from directory membership on each login (honors allow_nested_groups) and is additive with Authenticator.admin_users. If unset, admin is left to admin_users. |
None |
allow_nested_groups |
Recursively search for members within nested groups of allowed_groups (and admin_groups). |
False |
username_pattern |
Regular expression a valid username must match. If unset, any username is allowed. | None |
Home directory and auth state parameters
| Parameter | Description | Default |
|---|---|---|
create_user_home_dir |
Create the user's home directory at login if it does not exist. | False |
create_user_home_dir_cmd |
Command (a list of strings) used to create the home directory; the username is appended as the final argument. | ['mkhomedir_helper'] on linux |
auth_state_attributes |
LDAP attributes to fetch and expose to JupyterHub as auth_state. Requires Authenticator.enable_auth_state = True to be persisted. |
[] |
c.LDAPAuthenticator.create_user_home_dir = True
c.LDAPAuthenticator.auth_state_attributes = ['mail', 'displayName']
Home-directory command. The default
mkhomedir_helperonly creates a home directory for a user that already resolves as a local POSIX account (e.g. via SSSD/nslcd). In pure-LDAP deployments where users are not local system accounts,mkhomedir_helperfails and login returns a 500. In that case, use a command that also creates the account, for example:# create the system user and its home directory c.LDAPAuthenticator.create_user_home_dir_cmd = ['useradd', '--create-home']The username is appended as the final argument. The command runs as the user the JupyterHub process runs as, so it must have permission to create accounts (typically root).
Examples
FreeIPA Integration
# freeipa example
c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator'
c.LDAPAuthenticator.server_hosts = ['ldaps://ldap1.example.com:636', 'ldaps://ldap2.example.com:636']
c.LDAPAuthenticator.bind_user_dn = 'uid=imauser,cn=users,cn=accounts,dc=example,dc=com'
c.LDAPAuthenticator.bind_user_password = 'imapassword'
c.LDAPAuthenticator.user_search_base = 'cn=users,cn=accounts,dc=example,dc=com'
c.LDAPAuthenticator.user_search_filter = '(&(objectClass=person)(uid={username}))'
c.LDAPAuthenticator.user_membership_attribute = 'memberOf'
c.LDAPAuthenticator.group_search_base = 'cn=groups,cn=accounts,dc=example,dc=com'
c.LDAPAuthenticator.group_search_filter = '(&(objectClass=ipausergroup)(memberOf={group}))'
c.LDAPAuthenticator.allowed_groups = ['cn=jupyterhub-users,cn=groups,cn=accounts,dc=example,dc=com']
c.LDAPAuthenticator.admin_groups = ['cn=jupyterhub-admins,cn=groups,cn=accounts,dc=example,dc=com']
c.LDAPAuthenticator.allow_nested_groups = True
c.LDAPAuthenticator.username_pattern = '[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?'
c.LDAPAuthenticator.create_user_home_dir = True
c.LDAPAuthenticator.create_user_home_dir_cmd = ['mkhomedir_helper']
Active Directory Integration
# active directory example
c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator'
c.LDAPAuthenticator.server_hosts = ['ldaps://ldap1.example.com:636', 'ldaps://ldap2.example.com:636']
c.LDAPAuthenticator.bind_user_dn = 'CN=imauser,CN=Users,DC=example,DC=com'
c.LDAPAuthenticator.bind_user_password = 'imapassword'
c.LDAPAuthenticator.user_search_base = 'CN=Users,DC=example,DC=com'
c.LDAPAuthenticator.user_search_filter = '(&(objectCategory=person)(objectClass=user)(sAMAccountName={username}))'
c.LDAPAuthenticator.user_membership_attribute = 'memberOf'
c.LDAPAuthenticator.group_search_base = 'CN=Groups,DC=example,DC=com'
c.LDAPAuthenticator.group_search_filter = '(&(objectClass=group)(memberOf={group}))'
c.LDAPAuthenticator.allowed_groups = ['CN=jupyterhub-users,CN=Groups,DC=example,DC=com']
c.LDAPAuthenticator.allow_nested_groups = True
c.LDAPAuthenticator.username_pattern = '[a-zA-Z0-9_.][a-zA-Z0-9_.-]{8,20}[a-zA-Z0-9_.$-]?'
c.LDAPAuthenticator.create_user_home_dir = True
c.LDAPAuthenticator.create_user_home_dir_cmd = ['mkhomedir_helper']
OpenLDAP Integration (direct-bind)
Because OpenLDAP does not natively populate the memberOf attribute on user
objects, allowed_groups scoping is short-circuited below. This example also
uses the direct-bind strategy, avoiding a service account entirely:
# openldap example
c.JupyterHub.authenticator_class = 'ldapauthenticator.LDAPAuthenticator'
c.LDAPAuthenticator.server_hosts = ['ldaps://ldap1.example.com:636', 'ldaps://ldap2.example.com:636']
c.LDAPAuthenticator.bind_dn_template = 'uid={username},ou=People,dc=example,dc=com'
c.LDAPAuthenticator.username_pattern = '[a-zA-Z0-9_.][a-zA-Z0-9_.-]{0,252}[a-zA-Z0-9_.$-]?'
c.LDAPAuthenticator.create_user_home_dir = True
c.LDAPAuthenticator.create_user_home_dir_cmd = ['mkhomedir_helper']
Directory compatibility
This authenticator uses an LDAP simple bind (username/DN + password) over an
optionally TLS-secured connection, and resolves groups from a memberOf-style
attribute. Any directory that supports those works. It does not implement
SASL, Kerberos/GSSAPI, NTLM, or client-certificate (EXTERNAL) bind mechanisms.
| Directory | Supported | Notes |
|---|---|---|
| Active Directory | ✅ | Use a sAMAccountName search filter; memberOf is native. Modern/hardened AD often rejects cleartext simple bind — use server_tls_strategy='before_bind' or 'on_connect'. |
| FreeIPA / 389 Directory Server | ✅ | memberOf is native. Supports OTP (see below). |
| OpenLDAP | ✅ | Requires the memberof overlay for allowed_groups/nested groups; without it, authentication still works but group scoping does not. |
| JumpCloud LDAP-as-a-Service | ✅ | Standard simple bind + memberOf. |
| Okta (LDAP Interface) | ⚠️ | Works via Okta's LDAP Interface over LDAPS with Okta-specific base DNs. For native Okta, an OIDC authenticator is usually the better fit. |
| Google Workspace (Secure LDAP) | ⚠️ | Requires mutual TLS (client certificate) configured via server_tls_kwargs (local_certificate_file / local_private_key_file). |
| Azure AD / Entra ID (native) | ❌ | Cloud-only Entra ID exposes no LDAP endpoint at all — it speaks OIDC/SAML, so use an OAuth/OIDC authenticator (oauthenticator) instead. To use LDAP against an Entra tenant, add Entra Domain Services (Azure AD DS), a managed AD-compatible service that does speak LDAP — then the Active Directory row applies. |
Rule of thumb: if the directory accepts an LDAP simple bind over TLS and
exposes a memberOf-like attribute, it is compatible. Kerberos-only realms
and proprietary APIs are not.
Multi-factor authentication
This plugin does not implement MFA as a first-class feature — there is no separate OTP field, challenge-response step, or push handling (JupyterHub's login form is a single username/password step).
However, because the password field is passed through to the LDAP bind unchanged, "append your one-time code to your password" MFA works transparently with any directory that validates the appended code at bind time:
| MFA style | Works | How |
|---|---|---|
| Password + OTP concatenation | ✅ | User enters password123456; the directory validates the trailing code during bind |
| FreeIPA OTP | ✅ | FreeIPA's documented password+OTP bind model |
| Okta LDAP Interface MFA | ✅ | Append a TOTP code, or literally append push for Okta Verify push |
| RADIUS-fronted LDAP | ✅ | Same concatenation model |
| Interactive challenge-response | ❌ | Would require a second login prompt |
| Push-only with nothing appended | ❌ | Unless the directory triggers/blocks on the push during the bind |
In other words, MFA is a property of the directory that this plugin's pass-through bind preserves, rather than something the plugin performs itself.
Migrating from 0.x
Version 1.0 is backward compatible — existing configurations continue to work. Two changes are worth noting:
server_use_sslis deprecated in favor ofserver_tls_strategy. Settingserver_use_ssl = Truestill works but now emits a warning and is translated toserver_tls_strategy = 'on_connect'. Note the new defaultserver_tls_strategyisbefore_bind(STARTTLS); if you relied on the previous plaintext default, setserver_tls_strategy = 'insecure'explicitly.- The authenticator is now
async, requiring JupyterHub >= 2.0 and Python >= 3.10.
Development
This project uses a Makefile to drive common tasks. Run make help for the
full list.
make venv # create a virtualenv and install dependencies
make lint # run ruff (lint + format check) and mypy
make test # run the pytest suite
make build # build the sdist/wheel package
License
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
File details
Details for the file jupyterhub_ldap_authenticator-1.1.0.tar.gz.
File metadata
- Download URL: jupyterhub_ldap_authenticator-1.1.0.tar.gz
- Upload date:
- Size: 46.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3a4afdb701d94f80cfd1680f2fc017a2892c6bb7f642ace19ba2ed5438835ca8
|
|
| MD5 |
25072d56ffe5fb951d6a5944841ae77e
|
|
| BLAKE2b-256 |
263ba433e56fe4876c7983c81de31d2d52efd6f32a7b33359d44a3cddc9cd7ea
|