Source docs/ldap.md · 1de96aa

LDAP and Active Directory sign-in

This page is for administrators who want the people of their organisation to sign in to Libris with the account they already have in an LDAP directory (OpenLDAP, 389 Directory Server…) or in Active Directory. Every setting is described in configuration.md; what people see is in the user guide.

How it works

  1. The person picks the directory on the sign-in screen and types their directory login and password.
  2. Libris connects to the directory over TLS (ldaps://, or ldap:// with StartTLS) and checks the server certificate and host name.
  3. Libris binds with a service account and searches the search base with the user filter, the login being escaped before it goes into the filter. Exactly one entry must match.
  4. Libris binds again as that entry, with the password typed. An empty password is refused before this step: many directories treat a bind with an empty password as an anonymous bind and answer “success”.
  5. If groups are configured, the entry’s group attribute (memberOf) must contain an allowed group. The administrator group, if any, decides the Libris administrator role.
  6. The Libris account is found by the entry’s stable identifier (entryUUID or objectGUID), created if the installation allows it, and its name and address are refreshed from the directory. If the account has a Libris second step (code or security key), it is asked for now.

The password is never stored, never logged and never sent anywhere but to the directory. Referrals are not followed. An unknown login, two matching entries and a wrong password all get the same answer, so the sign-in screen cannot be used to list the people in the directory.

Before you start

  • A service account that can search the people (and read memberOf) under the search base. It needs no other right. On Active Directory any ordinary domain user can do this; give it a long random password.
  • TLS on the directory. Use ldaps://host:636, or ldap://host:389 with StartTLS. The host name in the address must be in the server certificate (an IP address is refused). If that certificate is signed by an internal CA, paste the CA certificate (PEM) into Authority certificate; otherwise the system trust store of the Libris image is used.
  • The Libris container must reach the directory on that port. With Docker, check from the API container: docker compose exec api python -c "import socket; socket.create_connection(('ldap.example.com', 636), 5)".
  • A licence seat per person who will get an account. Restrict who may sign in with an allowed group before turning on account creation.

OpenLDAP

The defaults assume the inetOrgPerson schema and the memberof overlay, which fills memberOf on people entries.

SettingExample
Directory typeOpenLDAP
Directory addressldaps://ldap.example.com
Service account DNcn=libris,ou=services,dc=example,dc=com
Search baseou=people,dc=example,dc=com
User filter(empty: (&(objectClass=inetOrgPerson)(uid={username})))
Attributes(empty: uid, mail, entryUUID, memberOf)
Allowed groupscn=libris-users,ou=groups,dc=example,dc=com
Administrator groupcn=libris-admins,ou=groups,dc=example,dc=com

A read-only service account, in LDIF (hash the password with slappasswd):

dn: cn=libris,ou=services,dc=example,dc=com
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: libris
userPassword: {SSHA}...

and an ACL that lets it read people, for example:

access to dn.subtree="ou=people,dc=example,dc=com"
    by dn.exact="cn=libris,ou=services,dc=example,dc=com" read
    by * break

Without the memberof overlay there is no memberOf attribute: leave the groups empty and restrict access in the user filter instead, e.g. (&(objectClass=inetOrgPerson)(uid={username})(employeeType=translator)).

To check the settings from a shell before entering them in Libris:

ldapsearch -H ldaps://ldap.example.com -D 'cn=libris,ou=services,dc=example,dc=com' -W \
  -b 'ou=people,dc=example,dc=com' '(&(objectClass=inetOrgPerson)(uid=jdupont))' uid mail entryUUID memberOf

Active Directory

SettingExample
Directory typeActive Directory
Directory addressldaps://dc01.corp.example.com (port 636; a certificate must be installed on the domain controllers)
Service account DNCN=svc-libris,OU=Service Accounts,DC=corp,DC=example,DC=com
Search baseOU=Staff,DC=corp,DC=example,DC=com
User filter(empty: sAMAccountName or userPrincipalName, people only, disabled accounts excluded)
Attributes(empty: sAMAccountName, mail, objectGUID, memberOf)
Allowed groupsCN=Libris Users,OU=Groups,DC=corp,DC=example,DC=com
Administrator groupCN=Libris Admins,OU=Groups,DC=corp,DC=example,DC=com

People sign in with jdupont or jdupont@corp.example.com. The DOMAIN\jdupont form is not supported.

memberOf lists direct memberships only. To admit the members of nested groups, leave Allowed groups empty and put the transitive-membership rule in the user filter:

(&(objectCategory=person)(objectClass=user)(sAMAccountName={username})(!(userAccountControl:1.2.840.113556.1.4.803:=2))(memberOf:1.2.840.113556.1.4.1941:=CN=Libris Users,OU=Groups,DC=corp,DC=example,DC=com))

Failed sign-ins count against the domain’s account lockout policy like any other bind. Libris’ own throttle stops a client after 20 failures for one name in five minutes, but set the lockout threshold with that in mind.

To check the settings from a Windows shell: Get-ADUser jdupont -Properties mail,memberOf,objectGUID.

Turn it on

  1. In Paramètres › Authentification unique, card Annuaire LDAP / Active Directory, fill in the settings above, tick Activer la connexion par l’annuaire, and save. Saving asks for a recent proof of your identity.
  2. Press Tester l’annuaire, first alone (connection, certificate and service account), then with a login in Identifiant à rechercher (the filter finds exactly that person). No password is tried by the test.
  3. Choose how accounts appear:
    • existing local accounts: open each one in Paramètres › Utilisateurs and press Rattacher à l’annuaire. Its books, second step and API tokens stay; its Libris password is forgotten;
    • new people: tick Créer les comptes à la première connexion depuis l’annuaire, with an allowed group.
  4. Sign in from a private window with a directory account, then with a local account, to check both doors.

Keep at least one local administrator account: if the directory is unreachable, it is the way in.

Troubleshooting

The API log (docker compose logs api) records every refusal with a reason code and never a password:

Log lineMeaning
ldap=unreachable reason=...No connection: address, port, firewall, or the TLS handshake failed (unknown CA, host name not in the certificate).
ldap=service_bind_refusedThe service account DN or password is wrong, or the account is locked.
ldap=lookup_refused entries=0 / entries=manyThe filter found nobody, or more than one entry: check the search base and the filter.
ldap=refused code=not_allowedThe person is not in an allowed group (check memberOf, and nested groups on AD).
ldap=missing_identifierThe entry has no stable identifier attribute: check Attribut d’identifiant stable.
ldap=refused code=unknown_accountAccount creation is off and no Libris account is linked to this person.
ldap=last_admin_keptThe administrator group no longer contains the last active administrator; the role was kept.

Account creations, links, role changes made by the administrator group, and changes to these settings are also written in the audit log.

What happens when…

  • someone leaves the organisation: once removed from the directory, disabled, or taken out of the allowed groups, they can no longer sign in. Sessions already open last until they expire (SESSION_DURATION_HOURS); deactivate the account in Libris to close them at once.
  • someone is renamed or moved in the directory: the account is found by its stable identifier, so the new name and address are picked up at the next sign-in. If the new name is taken by another Libris account, a suffix is kept.
  • the directory is replaced: identifiers change, so the new directory’s people are new to Libris, and an account already linked to the old directory cannot be moved to the new one from the interface. Plan such a migration before switching.