# How LDAP Works, Message by Message

Source: https://startwithidentity.com/guides/protocols/ldap-message-flow/
Last updated: 2026-11-05
License: content by Start with Identity. Cite the source URL.

---

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](https://www.rfc-editor.org/rfc/rfc4511). 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](https://startwithidentity.com/standards/ldap/).

## 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](https://www.rfc-editor.org/rfc/rfc4511#section-4.14), [RFC 4513 §3](https://www.rfc-editor.org/rfc/rfc4513#section-3) |
| 2 | `BindRequest` as service account | `BindResponse` resultCode 0 | [§4.2](https://www.rfc-editor.org/rfc/rfc4511#section-4.2) |
| 3 | `SearchRequest` for the username | `SearchResultEntry`, `SearchResultDone` | [§4.5](https://www.rfc-editor.org/rfc/rfc4511#section-4.5) |
| 4 | `BindRequest` as the user's DN | `BindResponse` 0 or 49 | [§4.2](https://www.rfc-editor.org/rfc/rfc4511#section-4.2), [RFC 4513 §5.1.3](https://www.rfc-editor.org/rfc/rfc4513#section-5.1.3) |
| 5 | `SearchRequest` for group membership | entries | [§4.5](https://www.rfc-editor.org/rfc/rfc4511#section-4.5) |
| 6 | `UnbindRequest` | (connection closed) | [§4.3](https://www.rfc-editor.org/rfc/rfc4511#section-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](https://www.rfc-editor.org/rfc/rfc4511#section-4.1.1)). Everything is BER-encoded ASN.1 ([§5.1](https://www.rfc-editor.org/rfc/rfc4511#section-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](https://www.rfc-editor.org/rfc/rfc4513#section-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](https://www.rfc-editor.org/rfc/rfc4511#section-4.2) |
| `name` | DN to bind as. Empty for anonymous bind or for SASL. | §4.2, [RFC 4513 §5.1](https://www.rfc-editor.org/rfc/rfc4513#section-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](https://www.rfc-editor.org/rfc/rfc4513#section-5.2), [RFC 4422](https://www.rfc-editor.org/rfc/rfc4422) |
| `resultCode` | `success` (0), `invalidCredentials` (49), `strongerAuthRequired` (8), `confidentialityRequired` (13), `unwillingToPerform` (53) and others. | [§4.1.9](https://www.rfc-editor.org/rfc/rfc4511#section-4.1.9), [§4.2.2](https://www.rfc-editor.org/rfc/rfc4511#section-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](https://www.openldap.org/software/man.cgi?query=ldapsearch):

```bash
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](https://www.rfc-editor.org/rfc/rfc4511#section-4.5.1.1) |
| `derefAliases` | Alias handling; `neverDerefAliases` is the predictable choice. | [§4.5.1.3](https://www.rfc-editor.org/rfc/rfc4511#section-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](https://www.rfc-editor.org/rfc/rfc4511#section-4.5.1.4) |
| `filter` | Structured filter (`and`, `or`, `not`, `equalityMatch`, `substrings`, `present`...). Its string form is RFC 4515. | [§4.5.1.7](https://www.rfc-editor.org/rfc/rfc4511#section-4.5.1.7), [RFC 4515 §3](https://www.rfc-editor.org/rfc/rfc4515#section-3) |
| `attributes` | What to return. Never ask for `userPassword`. | [§4.5.1.8](https://www.rfc-editor.org/rfc/rfc4511#section-4.5.1.8) |
| `searchResEntry`, `searchResDone` | One message per entry, then a done message with the result code. | [§4.5.2](https://www.rfc-editor.org/rfc/rfc4511#section-4.5.2) |
| `entryUUID` | Stable operational identifier for the entry ([RFC 4530](https://www.rfc-editor.org/rfc/rfc4530)); 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](https://www.rfc-editor.org/rfc/rfc4513#section-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](https://www.rfc-editor.org/rfc/rfc4513#section-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](https://www.rfc-editor.org/rfc/rfc4513#section-5.1.2), [§6.3.1](https://www.rfc-editor.org/rfc/rfc4513#section-6.3.1)).
3. **Escape every user-supplied value**: filter values per [RFC 4515 §3](https://www.rfc-editor.org/rfc/rfc4515#section-3) (`*` → `\2a`, `(` → `\28`, `)` → `\29`, `\` → `\5c`, NUL → `\00`) and DN values per [RFC 4514 §2.4](https://www.rfc-editor.org/rfc/rfc4514#section-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](https://www.rfc-editor.org/rfc/rfc4511#section-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](https://www.rfc-editor.org/rfc/rfc4511#section-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](https://startwithidentity.com/cves/cve-2024-49113/) (LDAPNightmare), [CVE-2024-49112](https://startwithidentity.com/cves/cve-2024-49112/) |
| Directory ACLs allow anonymous or low-privilege writes | Attacker-created entries become privileged principals | [CVE-2026-76578](https://startwithidentity.com/cves/cve-2026-76578/) (FreeIPA) |
| Input evaluated before the caller's rights are checked | Any authenticated principal reaches sensitive server state | [CVE-2026-79678](https://startwithidentity.com/cves/cve-2026-79678/) (FreeIPA) |
| Different errors for unknown and known users | Username enumeration feeds spraying | [CVE-2023-3462](https://startwithidentity.com/cves/cve-2023-3462/) (Vault LDAP auth) |
| Lockout keyed differently from login | Case permutation resets the lockout counter | [CVE-2025-6004](https://startwithidentity.com/cves/cve-2025-6004/) (Vault) |
| Password binds exposed to the internet | High-volume guessing against the directory | [Password spraying](https://startwithidentity.com/techniques/password-spraying/), [credential stuffing](https://startwithidentity.com/techniques/credential-stuffing/) |
| Directory readable by every domain user | Reconnaissance for service accounts and delegation | [Kerberoasting](https://startwithidentity.com/techniques/kerberoasting/), [Kerberos delegation abuse](https://startwithidentity.com/techniques/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](https://www.openldap.org/software/man.cgi?query=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](https://startwithidentity.com/standards/ldap/), [how Kerberos works, message by message](https://startwithidentity.com/guides/protocols/kerberos-message-flow/), [how RADIUS works, message by message](https://startwithidentity.com/guides/protocols/radius-message-flow/), [SCIM message flow](https://startwithidentity.com/guides/protocols/scim-message-flow/). Glossary: [LDAP](https://startwithidentity.com/glossary/ldap/), [directory service](https://startwithidentity.com/glossary/directory-service/), [Active Directory](https://startwithidentity.com/glossary/active-directory/).
