Skip to content
Start with Identity
Protocols · Advanced

How LDAP Works, Message by Message

By SWI Community Team · Updated 2026-11-05 · 11 min

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

  1. 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.
  2. 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).
  3. 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.
  4. Exactly one entry. Treat zero or more than one search result as a failed login, not "use the first".
  5. 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.
  6. Do not follow referrals with credentials to servers you did not configure (RFC 4511 §6, §4.1.10).
  7. Key accounts on a stable identifier (entryUUID or objectGUID), not uid, mail or the DN, which change on rename or move.
  8. Least-privilege service account: read-only, scoped to the subtrees the application needs, with a rotated credential or SASL EXTERNAL/GSSAPI instead 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.

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.
Last reviewed By SWI Community TeamSuggest a correctionHow we research