LDAP is the odd one out among the ways to sign in to Directus. There is no browser redirect, no consent screen, no client secret and no callback URL. The user types a username and password into the Directus login form, and Directus asks your directory whether that pair is real. Everything that makes OAuth fiddly is absent — and it is replaced by a different set of things that go wrong, most of them defaults nobody thinks to check.
This guide was written against a live Directus 11.17.4 talking to a real OpenLDAP server. Every behaviour described below — which attribute is searched, how deep, what happens to a user who is not in any group — was confirmed by running it, not by reading the documentation.
How the LDAP driver actually works
Three separate connections happen on every login, and knowing which is which turns most error messages into an obvious diagnosis.
One: the service account binds. Directus connects as AUTH_LDAP_BIND_DN and immediately performs a healthcheck — it searches for the bind account’s own record. If that record does not exist, login fails before your user is ever considered. More on this below, because it is the single most confusing failure in the whole driver.
Two: your user is looked up. Using the service account, Directus searches under AUTH_LDAP_USER_DN for an entry whose cn matches what was typed in the login box, and reads five attributes: uid, first name, last name, mail and userAccountControl.
Three: the password is checked by binding as that user. Directus takes the DN it just found and tries to bind with the supplied password. Directus never sees, stores or hashes the password — the directory is the only thing that validates it. That is the genuine security advantage of LDAP over every other provider here, and it is worth saying out loud to whoever signs off on the setup.
A configuration that works
Five variables is the whole minimum. Nothing else is required.
AUTH_PROVIDERS="ldap"AUTH_LDAP_DRIVER="ldap"
AUTH_LDAP_CLIENT_URL="ldap://ldap.internal.example.com:389"
AUTH_LDAP_BIND_DN="cn=directus,ou=users,dc=example,dc=com"
AUTH_LDAP_BIND_PASSWORD="the-service-account-password"
AUTH_LDAP_USER_DN="ou=users,dc=example,dc=com"
Restart, and a second button appears under the Directus login form. There is no redirect URI to register anywhere, because there is no redirect.
Two variables you will almost certainly want as well:
AUTH_LDAP_USER_SCOPE="sub"
AUTH_LDAP_DEFAULT_ROLE_ID="<a role UUID>"Why each of those matters is the rest of this article.
The bind account must be a real entry, not just a rootDN
This is the one that costs people an afternoon, and it produces an error message that points in entirely the wrong direction:
Service "ldap" is unavailable. Service returned unexpected error: Code: 0x20.0x20 is decimal 32, which is LDAP’s noSuchObject. Not "wrong password", not "cannot connect" — the object is not there. And the object it means is your bind account.
After binding, Directus searches the bind DN itself with scope base to confirm it can read its own record. On OpenLDAP the admin account you were given — typically cn=admin,dc=example,dc=com — is the rootDN. It is a credential defined in the server configuration, not an entry in the directory tree. Binding as it succeeds. Searching for it returns nothing.
You can prove which situation you are in without touching Directus at all:
# does the bind succeed?
ldapwhoami -x -H ldap://your-server:389 -D "cn=admin,dc=example,dc=com" -w secret# does the account exist as an entry?
ldapsearch -x -H ldap://your-server:389 -D "cn=admin,dc=example,dc=com" -w secret \
-b "cn=admin,dc=example,dc=com" -s base
If the first prints a DN and the second says No such object, that is exactly this problem. Create a normal service account entry inside the tree and bind as that instead.
Active Directory users rarely hit this, because in AD the service account is always a real user object. That is precisely why the tutorials written against AD never mention it, and why it blindsides everyone on OpenLDAP.
The username is matched on cn, not uid
AUTH_LDAP_USER_ATTRIBUTE defaults to cn. Most people running OpenLDAP assume it is uid, because that is what a username looks like. If your directory stores logins in uid, set it explicitly:
AUTH_LDAP_USER_ATTRIBUTE="uid"On Active Directory the value you almost always want is sAMAccountName, which is the pre-Windows-2000 login name your users already know. userPrincipalName works too if people are used to signing in with an email-shaped address.
A curiosity worth knowing, because it surprised us in testing: cn is multi-valued. A directory entry carrying both cn: alice and cn: User1 can be signed into with either string, because the filter is a plain equality match and LDAP matches any value of the attribute. If you need exactly one accepted username, point AUTH_LDAP_USER_ATTRIBUTE at a single-valued attribute like uid or sAMAccountName.
Users in sub-OUs are invisible by default
AUTH_LDAP_USER_SCOPE defaults to one — one level only. Entries sitting directly under AUTH_LDAP_USER_DN are found. Anything one level deeper is not.
We put a user at cn=dave,ou=berlin,ou=users,dc=… with AUTH_LDAP_USER_DN="ou=users,dc=…". Dave could not log in. The error was Invalid user credentials — identical to a wrong password, and identical to a user who does not exist. Nothing anywhere said "I looked, but not that deep".
Adding one line fixed it instantly:
AUTH_LDAP_USER_SCOPE="sub"Any organisation that splits users by office, department or country has a nested tree. If some people can sign in and others get invalid credentials with a password you have personally reset, check the scope before you check anything else. AUTH_LDAP_GROUP_SCOPE defaults to one as well, and has the same problem for nested group containers.
Groups map to roles by name — no JSON required
This is the nicest part of the LDAP driver, and it works quite differently from the OpenID and OAuth2 ones, which need a json:-prefixed mapping object.
Point Directus at your groups:
AUTH_LDAP_GROUP_DN="ou=groups,dc=example,dc=com"Directus finds every group the user belongs to, takes the cn of each, and looks for a Directus role whose name matches, case-insensitively. A directory group called Editor grants the Directus role called Editor. That is the entire mechanism. There is no mapping table to maintain, and renaming a role in Directus silently breaks it — which is the trade-off.
We verified this end to end: a user in an LDAP group cn=Editor received the Directus role named Editor on their next login, and a user in no matching group received nothing.
Two details that decide whether this works on your directory:
AUTH_LDAP_GROUP_ATTRIBUTE defaults to member, which holds full DNs — correct for groupOfNames and for Active Directory. If your groups are posixGroup, they use memberUid and store bare usernames instead. Directus special-cases exactly this: set AUTH_LDAP_GROUP_ATTRIBUTE="memberUid" and it will search using the user’s uid rather than their DN. Any other attribute name and it always searches by DN.
Roles are matched on name, not on any identifier, so "Editor" and "editor" both match but "Editors" does not.
The thing to understand before you enable this
LDAP has no public-registration switch. The OpenID and OAuth2 drivers both refuse to create a user unless you set ALLOW_PUBLIC_REGISTRATION to true. The LDAP driver has no such option anywhere in its code. If someone exists in the directory subtree you pointed at and can bind with their own password, Directus creates them an account, on the spot, without asking.
That is usually what you want — it is why you connected a directory in the first place. But it means the boundary around your Directus instance is now exactly the boundary of AUTH_LDAP_USER_DN. Point it at the root of a 4,000-person company directory and every one of those people can create themselves an account.
Two ways to keep it sane. Point AUTH_LDAP_USER_DN at an OU that contains only the people who should have access, so the directory itself is the allow-list. Or leave AUTH_LDAP_DEFAULT_ROLE_ID unset and grant roles solely through group mapping — anyone outside the mapped groups then arrives with no role and can see nothing.
Which brings us to the second half of that: a user with no role logs in successfully and then sees an empty Directus. No error, no explanation, just nothing. We reproduced this exactly — the account was created, the login returned a valid token, and the role column was null. If your first test user reports that Directus "looks broken", they probably have no role.
What ends up in the Directus user record
The external_identifier is the user’s full DN — for example cn=alice,ou=users,dc=example,dc=com. Not their username, not their email. This has a consequence worth planning for: moving a person between OUs changes their DN, and Directus will treat them as a brand-new user, with a new account and none of their old ownership. There is no way around this in the driver; it is the identifier it uses.
Names and email come from three attributes with these defaults:
AUTH_LDAP_FIRST_NAME_ATTRIBUTE givenName
AUTH_LDAP_LAST_NAME_ATTRIBUTE sn
AUTH_LDAP_MAIL_ATTRIBUTE mailAnything missing from the directory simply arrives empty. In our test one user had no givenName and no mail, and the resulting Directus account had a blank first name and no email address at all — which is legal in Directus for an external user, but does mean that person cannot receive a notification.
By default those attributes are read once, at account creation, and never refreshed. To re-read them on every login, set AUTH_LDAP_SYNC_USER_INFO="true".
Active Directory specifics
Everything above applies, with four changes.
Use sAMAccountName as the user attribute. Use sub scope, because AD trees are always nested. Expect member to be correct for groups, since AD uses full DNs.
And AD alone gets an extra check: Directus reads userAccountControl and refuses the session if the account is disabled, locked out or has an expired password. On a plain OpenLDAP that attribute does not exist, so no such check happens — a detail worth knowing if you assumed disabling a directory account would lock someone out of Directus. On OpenLDAP, it will not.
For LDAPS, use an ldaps:// URL on port 636. Client options nest with a double underscore, so TLS settings look like this:
AUTH_LDAP_CLIENT_URL="ldaps://ad.example.com:636"
AUTH_LDAP_CLIENT__TLS_OPTIONS__REJECT_UNAUTHORIZED="false"That last one disables certificate verification. It is a debugging tool for a self-signed internal CA, not a setting to leave on — you are turning off the check that stops someone impersonating your directory.
Trying it on localhost first
LDAP is by far the easiest of the Directus providers to test locally, because nothing has to be reachable from the internet. No redirect URI, no public callback, no HTTPS. A directory container and a Directus container on the same Docker network is a complete test rig.
The one thing to get right is that AUTH_LDAP_CLIENT_URL is resolved by Directus, not by your browser — so inside Compose it is the service name and the internal port, ldap://openldap:1389, even though you reach Directus itself on localhost. This is the opposite of the OAuth providers, where PUBLIC_URL has to be what the browser sees.
Because there is no browser redirect, you can test the whole thing with curl and never open the admin app:
curl -X POST http://localhost:8055/auth/login/ldap \
-H "Content-Type: application/json" \
-d '{"identifier":"alice","password":"alicepass"}'A working setup returns an access token. That single command distinguishes a directory problem from a Directus problem in about a second, and it is how every finding in this article was checked.
Reading the errors
The driver deliberately collapses several different failures into one message, so that an attacker cannot use the login form to work out which usernames exist. Helpful for security, unhelpful for you.
Invalid user credentials means any of: wrong password, no such user, the user is outside your scope, or the user attribute does not match what they typed. Check scope and user attribute before you assume the password is wrong.
Service "ldap" is unavailable with a Code: in it is the directory refusing or failing something. 0x20 is noSuchObject — usually the bind account, as above. Connection refused means the URL or port is wrong from Directus’s point of view, which in Docker is a different point of view from yours.
Turn on LOG_LEVEL="debug" while setting this up. The driver logs [LDAP] lines that name what it was doing when it gave up.
The licence tier, but only on Directus 12
Directus 11 has no licence module at all — LDAP is ungated, and everything above runs on a stock 11.17.4. Licensing arrived in 12.0.0, where the SSO routes sit behind an sso_enabled entitlement. Without it the LDAP login route returns 404 and Directus logs you have SSO providers configured these will be unavailable under the current license tier at startup. If you are on 12 and the LDAP button never appears, that is the first thing to check.
Common questions
Does Directus support LDAP and Active Directory?
Yes. Directus ships an ldap driver as one of its five authentication drivers, and it covers both OpenLDAP and Active Directory. You set AUTH_LDAP_DRIVER to ldap, give it a client URL, a bind account and a user DN, and a second login option appears under the Directus login form. Unlike the OAuth providers there is no redirect URI and no callback.
Does Directus ever see the user password?
No. Directus validates a password by attempting an LDAP bind as that user against your directory. It does not store, hash or log the password, and the directory is the only thing that decides whether it is correct. This is the main security argument for using LDAP rather than local Directus accounts.
Why do I get Service "ldap" is unavailable with Code: 0x20?
Code 0x20 is decimal 32, LDAP noSuchObject, and it almost always means your bind account is not a real entry in the directory. After binding, Directus searches for the bind account’s own record as a healthcheck. On OpenLDAP the admin account is usually the rootDN, which is a configured credential rather than an entry in the tree — binding succeeds but the search finds nothing. Create a normal service account inside the tree and bind as that.
Why can some LDAP users log in but not others?
Most often the scope. AUTH_LDAP_USER_SCOPE defaults to one, meaning only entries directly under AUTH_LDAP_USER_DN are found. Anyone in a nested OU gets Invalid user credentials, which is the same message as a wrong password. Set AUTH_LDAP_USER_SCOPE to sub to search the whole subtree.
Which attribute does Directus match the username against?
AUTH_LDAP_USER_ATTRIBUTE, which defaults to cn. On OpenLDAP you usually want uid, and on Active Directory you usually want sAMAccountName. Note that cn is multi-valued, so an entry with two cn values can be signed into with either one.
How do I map LDAP groups to Directus roles?
Set AUTH_LDAP_GROUP_DN to the container holding your groups. Directus reads the cn of each group the user belongs to and matches it, case-insensitively, against Directus role names — a group called Editor grants the role called Editor. No JSON mapping is needed, unlike the OpenID and OAuth2 drivers. Use AUTH_LDAP_GROUP_ATTRIBUTE="memberUid" if your groups are posixGroup rather than groupOfNames.
Can I stop LDAP users creating Directus accounts automatically?
Not with a setting — the LDAP driver has no public registration option at all, so anyone who exists under AUTH_LDAP_USER_DN and can bind gets an account created on first login. Control it by pointing AUTH_LDAP_USER_DN at an OU containing only the people who should have access, and by leaving AUTH_LDAP_DEFAULT_ROLE_ID unset so unmapped users arrive with no role and no visibility.
What happens if I move a user to a different OU?
Directus stores the user’s full DN as their external identifier, so changing their DN makes them a new user. They will get a fresh Directus account on next login and will not inherit anything owned by the old one. Plan OU moves accordingly.
Can I test Directus LDAP login on localhost?
Yes, and it is the easiest provider to test because there is no browser redirect. Run a directory container alongside Directus and set AUTH_LDAP_CLIENT_URL to the service name on the Docker network, not localhost — Directus resolves it, not your browser. Then POST an identifier and password to /auth/login/ldap with curl; a working setup returns an access token.
Connecting Directus to a corporate directory?
Directory work is where Directus projects quietly get expensive: the scope is never just "turn on LDAP", it is deciding which OU is the boundary, which groups become which roles, and what a user sees on the day they lose one. We build on Directus full time and have done this on real directories. Fifteen minutes and you will know what yours actually needs.
Book a 15-minute call →