How LDAP Works, Message by Message
An application that "logs users in with LDAP" runs a short, fixed conversation with the directory: upgrade the connection to TLS, bind as a service account, search for the user's entry, bind again as that entry with the password the user typed, and read the result code. Every step is an LDAPMessage from RFC 4511. This page walks through each one with fake values, ties every field to its RFC section, and lists the checks the application must run. For what LDAP is and where it fits, see the LDAP standard page.
The sequence at a glance
| # | Client → server | Server → client | Spec |
|---|---|---|---|
| 1 | ExtendedRequest StartTLS (OID 1.3.6.1.4.1.1466.20037) |
ExtendedResponse success, then TLS handshake |
RFC 4511 §4.14, RFC 4513 §3 |
| 2 | BindRequest as service account |
BindResponse resultCode 0 |
§4.2 |
| 3 | SearchRequest for the username |
SearchResultEntry, SearchResultDone |
§4.5 |
| 4 | BindRequest as the user's DN |
BindResponse 0 or 49 |
§4.2, RFC 4513 §5.1.3 |
| 5 | SearchRequest for group membership |
entries | §4.5 |
| 6 | UnbindRequest |
(connection closed) | §4.3 |
Many applications use LDAPS on port 636 instead of step 1; the messages after the TLS handshake are identical.
The envelope every message shares
LDAPMessage ::= SEQUENCE {
messageID MessageID, -- INTEGER, chosen by the client
protocolOp CHOICE { bindRequest, bindResponse, searchRequest, ... },
controls [0] Controls OPTIONAL }
messageID is unique per outstanding request and is how responses are matched to requests on a pipelined connection (§4.1.1–4.1.1.1). Everything is BER-encoded ASN.1 (§5.1); the examples below are decoded into readable form.
Step 1: StartTLS
messageID 1
extendedReq { requestName "1.3.6.1.4.1.1466.20037" } -- no requestValue
messageID 1
extendedResp { resultCode success (0), matchedDN "", diagnosticMessage "",
responseName "1.3.6.1.4.1.1466.20037" }
After a success response the client starts the TLS handshake on the same connection. It must then check the server's identity against the certificate as RFC 4513 §3.1.3 describes, and RFC 4511 §6 advises clients to reject referrals returned by the StartTLS operation. A client that falls back to cleartext when StartTLS fails has no protection against an attacker who simply blocks it.
Step 2: bind as the service account
messageID 2
bindRequest {
version 3,
name "cn=svc-portal,ou=service,dc=example,dc=com",
authentication simple "not-a-real-password-example" }
messageID 2
bindResponse { resultCode success (0), matchedDN "", diagnosticMessage "" }
| Field | Meaning | Spec |
|---|---|---|
version |
Always 3. No negotiation; a server that does not support it returns protocolError. |
RFC 4511 §4.2 |
name |
DN to bind as. Empty for anonymous bind or for SASL. | §4.2, RFC 4513 §5.1 |
authentication simple |
The password as an OCTET STRING. Only safe inside TLS. | RFC 4513 §5.1.3 |
authentication sasl (alternative) |
mechanism such as GSSAPI (Kerberos) or EXTERNAL (TLS client certificate), with credentials. |
§4.2, RFC 4513 §5.2, RFC 4422 |
resultCode |
success (0), invalidCredentials (49), strongerAuthRequired (8), confidentialityRequired (13), unwillingToPerform (53) and others. |
§4.1.9, §4.2.2 |
SASL EXTERNAL with a client certificate, or GSSAPI with a Kerberos keytab, removes the service account password altogether.
Step 3: find the user
The command-line equivalent with OpenLDAP's ldapsearch:
ldapsearch -ZZ -H ldap://dc1.example.com \
-D "cn=svc-portal,ou=service,dc=example,dc=com" -W \
-b "ou=people,dc=example,dc=com" -s sub -z 2 \
"(&(objectClass=person)(uid=ada))" dn uid mail entryUUID
On the wire:
messageID 3
searchRequest {
baseObject "ou=people,dc=example,dc=com",
scope wholeSubtree (2),
derefAliases neverDerefAliases (0),
sizeLimit 2,
timeLimit 10,
typesOnly FALSE,
filter and { equalityMatch { objectClass, "person" },
equalityMatch { uid, "ada" } },
attributes [ "uid", "mail", "entryUUID" ] }
messageID 3
searchResEntry {
objectName "uid=ada,ou=people,dc=example,dc=com",
attributes [ uid: "ada", mail: "ada@example.com",
entryUUID: "597ae2f6-16a6-1027-98f4-abcdefabcdef" ] }
messageID 3
searchResDone { resultCode success (0), matchedDN "", diagnosticMessage "" }
| Field | Meaning | Spec |
|---|---|---|
baseObject, scope |
Where to search and how deep: baseObject (0), singleLevel (1), wholeSubtree (2). |
§4.5.1.1–4.5.1.2 |
derefAliases |
Alias handling; neverDerefAliases is the predictable choice. |
§4.5.1.3 |
sizeLimit, timeLimit |
Caps set by the client. A limit of 2 lets you detect an ambiguous username. | §4.5.1.4–4.5.1.5 |
filter |
Structured filter (and, or, not, equalityMatch, substrings, present...). Its string form is RFC 4515. |
§4.5.1.7, RFC 4515 §3 |
attributes |
What to return. Never ask for userPassword. |
§4.5.1.8 |
searchResEntry, searchResDone |
One message per entry, then a done message with the result code. | §4.5.2 |
entryUUID |
Stable operational identifier for the entry (RFC 4530); Active Directory's equivalent is objectGUID. |
RFC 4530 |
Step 4: bind as the user
messageID 4
bindRequest { version 3, name "uid=ada,ou=people,dc=example,dc=com",
authentication simple "<the password the user typed>" }
messageID 4
bindResponse { resultCode invalidCredentials (49), matchedDN "",
diagnosticMessage "" }
Result 49 means the DN is valid syntax but the credentials are not (RFC 4513 §5.1.3). Return the same error to the user whether the account does not exist or the password is wrong; diagnosticMessage and differing result codes can reveal which, a disclosure RFC 4511 §6 warns about.
Do this bind on a separate connection, or rebind as the service account afterwards: the bind changes the connection's authorization state (RFC 4513 §4), and any later group lookup would run with the user's rights.
The checks an LDAP-authenticating application must run
- TLS first, verified. Complete StartTLS or LDAPS and validate the server certificate and name before any bind (RFC 4513 §3.1.3; RFC 4511 §6). Never fall back to cleartext.
- Reject empty passwords before sending the bind. An empty password turns a simple bind into the unauthenticated mechanism, which authenticates nobody (RFC 4513 §5.1.2, §6.3.1).
- Escape every user-supplied value: filter values per RFC 4515 §3 (
*→\2a,(→\28,)→\29,\→\5c, NUL →\00) and DN values per RFC 4514 §2.4. - Exactly one entry. Treat zero or more than one search result as a failed login, not "use the first".
- Success means resultCode 0 on the user bind, nothing else (RFC 4511 §4.1.9). Do not infer success from the absence of an exception.
- Do not follow referrals with credentials to servers you did not configure (RFC 4511 §6, §4.1.10).
- Key accounts on a stable identifier (
entryUUIDorobjectGUID), notuid,mailor the DN, which change on rename or move. - Least-privilege service account: read-only, scoped to the subtrees the application needs, with a rotated credential or SASL
EXTERNAL/GSSAPIinstead of a password.
Common implementation mistakes, with the CVEs behind them
| Mistake | What goes wrong | Real examples |
|---|---|---|
| Client or server that cannot handle malformed BER | A crafted response crashes or compromises a domain controller; RFC 4511 §6 says peers MUST handle invalid and arbitrary-length encodings | CVE-2024-49113 (LDAPNightmare), CVE-2024-49112 |
| Directory ACLs allow anonymous or low-privilege writes | Attacker-created entries become privileged principals | CVE-2026-76578 (FreeIPA) |
| Input evaluated before the caller's rights are checked | Any authenticated principal reaches sensitive server state | CVE-2026-79678 (FreeIPA) |
| Different errors for unknown and known users | Username enumeration feeds spraying | CVE-2023-3462 (Vault LDAP auth) |
| Lockout keyed differently from login | Case permutation resets the lockout counter | CVE-2025-6004 (Vault) |
| Password binds exposed to the internet | High-volume guessing against the directory | Password spraying, credential stuffing |
| Directory readable by every domain user | Reconnaissance for service accounts and delegation | Kerberoasting, Kerberos delegation abuse |
Try it locally
SWI has no browser tool for LDAP, because a browser cannot open raw TCP connections. Use OpenLDAP's ldapsearch against a test directory: -ZZ requires StartTLS to succeed, -d 1 prints protocol debugging, and binding with -w "" shows how your server answers an unauthenticated bind. If it returns success, fix the server configuration before an application relies on it.
Related
LDAP standard page, how Kerberos works, message by message, how RADIUS works, message by message, SCIM message flow. Glossary: LDAP, directory service, Active Directory.
Frequently asked questions
- What LDAP messages does a login against a directory use?
- The common search-then-bind pattern uses StartTLS (an ExtendedRequest), a BindRequest as a service account, a SearchRequest to find the user's DN, a SearchResultEntry and SearchResultDone in reply, a second BindRequest as the user's DN with the password they typed, a BindResponse whose resultCode is 0 for success or 49 for invalidCredentials, and finally an UnbindRequest. All are LDAPMessage structures defined in RFC 4511.
- Why does an LDAP bind with an empty password succeed?
- Because a bind with a DN and a zero-length password is the unauthenticated mechanism of simple bind defined in RFC 4513 section 5.1.2. Some servers return success and give the client an anonymous session; the DN is not authenticated. An application that treats success as proof of the password lets anyone log in with an empty password. Reject empty passwords in the client and disable unauthenticated binds on the server.
- How do I prevent LDAP injection?
- Escape every value you put into a search filter as RFC 4515 section 3 requires: the characters asterisk, left and right parenthesis, backslash and NUL become a backslash followed by two hex digits, for example \2a for an asterisk. Escape values placed in DNs with the rules of RFC 4514 section 2.4. Better, use a library that builds filters from parameters.
Related on Start with Identity
- GuideHow OpenID Connect Works, Message by Message
OpenID Connect on the wire: discovery, the authentication request, the callback, the token request with PKCE, the token response and a decoded ID token, with ev
- GuideHow SAML 2.0 Works, Message by Message
A field-by-field walkthrough of the SAML 2.0 Web Browser SSO profile: the AuthnRequest, the Response and Assertion, the bindings that carry them, the eight chec
- GuideHow OAuth 2.0 and OAuth 2.1 Work, Message by Message
OAuth on the wire: the authorization code request with PKCE, the token request and response, client credentials, refresh with rotation, the API call, JWT access
- BlogGitLab patches a SAML-to-LDAP account takeover and OIDC discovery cache poisoning
GitLab's October 21 critical patch release fixes CVE-2026-15723, which let a signed-in user take over an LDAP-backed account when OmniAuth SAML and LDAP auto-li
- ArticleReusable Identity Verification: How Verify-Once-Reuse-Many Works
Reusable identity verification lets people verify once and reuse the result across services. How it works, the cost and fraud economics, the role of government
- ArticleTop 6 Open-Source Directory Services and LDAP Servers
The best open-source directory servers in 2026, from OpenLDAP and 389 Directory Server to FreeIPA, Samba AD, Kanidm, and LLDAP, compared on protocol support, Ac