Gumdrop provides a fully asynchronous LDAPv3 client and a companion
LDAPRealm that integrates LDAP-based authentication and
authorisation into the framework's centralised realm interface. Both
are built on gumdrop's non-blocking I/O architecture and use BER encoding
(ITU-T X.690) for wire-level communication.
The LDAP client follows the same asynchronous, callback-driven patterns as other Gumdrop clients (Redis, SMTP, HTTP). The connection progresses through a series of typed interfaces that enforce the correct protocol sequence.
org.bluezoo.gumdrop.ldap.client.LDAPClient — connection
factory; configures transport, TLS, and initiates the connectionorg.bluezoo.gumdrop.ldap.client.LDAPClientProtocolHandler
— protocol handler implementing BER message encoding/decoding and the
three state interfacesorg.bluezoo.gumdrop.ldap.client.LDAPConnected —
interface available immediately after connection; offers
bind(), bindSASL(), startTLS(),
and unbind()org.bluezoo.gumdrop.ldap.client.LDAPPostTLS —
interface available after STARTTLS; same bind operations, no second
STARTTLSorg.bluezoo.gumdrop.ldap.client.LDAPSession —
interface available after a successful bind; provides search, modify, add,
delete, compare, modifyDN, extended, rebind, and unbindorg.bluezoo.gumdrop.ldap.client.LDAPConnectionReady —
entry-point callback receiving the initial LDAPConnectedorg.bluezoo.gumdrop.ldap.asn1 — BER encoder/decoder
used for all wire-level encoding
connect()
|
v
LDAPConnected ──startTLS()──> LDAPPostTLS
| |
├── bind() ──────────────────────┤
├── bindSASL() ──────────────────┤
└── bindAnonymous() ─────────────┘
|
v
LDAPSession
|
┌───────────────┼───────────────┐
v v v
search() modify() rebind()
add() delete() rebindSASL()
compare() modifyDN() unbind()
extended()
Create an LDAPClient, configure it, then call
connect() with a handler:
LDAPClient client = new LDAPClient(selectorLoop, "ldap.example.com", 389);
client.connect(new LDAPConnectionReady() {
@Override
public void handleReady(LDAPConnected connection) {
connection.bind("cn=admin,dc=example,dc=com", "secret",
new BindResultHandler() {
@Override
public void handleBindSuccess(LDAPSession session) {
// Authenticated — perform directory operations
}
@Override
public void handleBindFailure(LDAPResult result,
LDAPConnected conn) {
System.err.println("Bind failed: "
+ result.getDiagnosticMessage());
conn.unbind();
}
});
}
@Override
public void onConnected(Endpoint endpoint) {
// TCP connection established
}
@Override
public void onSecurityEstablished(SecurityInfo info) {
// TLS handshake complete
}
@Override
public void onError(Exception cause) {
cause.printStackTrace();
}
@Override
public void onDisconnected() {
// Connection closed
}
});
| Mode | Default Port | Description |
|---|---|---|
| Plaintext LDAP | 389 | Standard LDAPv3 (RFC 4511). Optionally upgrade to TLS via STARTTLS. |
| LDAPS (implicit TLS) | 636 | TLS established before any LDAP traffic (RFC 4513 §3.1.3).
Set client.setSecure(true). |
Simple bind sends a distinguished name and password in the clear (or over TLS). This is the most common authentication method:
connection.bind("cn=admin,dc=example,dc=com", "secret", bindHandler);
connection.bindAnonymous(bindHandler);
SASL bind uses a challenge-response mechanism to authenticate without sending
the password in the clear. Gumdrop implements client-side SASL using its own
cryptographic primitives (SASLUtils), so all mechanism evaluation
is non-blocking and safe for the NIO event loop.
| Mechanism | RFC | Description |
|---|---|---|
PLAIN |
RFC 4616 | Single-step: sends \0authcid\0password. Requires TLS. |
CRAM-MD5 |
RFC 2195 | Server sends a challenge; client responds with
username HMAC-MD5(password, challenge). |
DIGEST-MD5 |
RFC 2831 | Multi-step md5-sess digest with nonce, cnonce, and digest-uri. Does not require TLS. |
EXTERNAL |
RFC 4422 App. A | Relies on TLS client certificate. No credentials transmitted. |
GSSAPI |
RFC 4752 | Kerberos V5 authentication. Requires a JAAS Subject with valid
credentials. The first evaluateChallenge() call may contact
the KDC; use bindSASL(mechanism, callback, executor) to
offload to a worker thread. |
import org.bluezoo.gumdrop.auth.SASLClientMechanism;
import org.bluezoo.gumdrop.auth.SASLUtils;
SASLClientMechanism mechanism =
SASLUtils.createClient("DIGEST-MD5", "admin", "secret",
"ldap.example.com");
connection.bindSASL(mechanism, new BindResultHandler() {
@Override
public void handleBindSuccess(LDAPSession session) {
// Authenticated via DIGEST-MD5
}
@Override
public void handleBindFailure(LDAPResult result,
LDAPConnected conn) {
System.err.println("SASL bind failed: "
+ result.getDiagnosticMessage());
conn.unbind();
}
});
The protocol handler manages intermediate
SASL_BIND_IN_PROGRESS (result code 14) responses internally
and only invokes the callback on final success or failure.
After binding, the LDAPSession interface provides search with
the full RFC 4511 §4.5 syntax:
SearchRequest search = new SearchRequest();
search.setBaseDN("dc=example,dc=com");
search.setScope(SearchScope.SUBTREE);
search.setFilter("(&(objectClass=person)(uid=alice))");
search.setAttributes("cn", "mail", "memberOf");
search.setSizeLimit(10);
session.search(search, new SearchResultHandler() {
@Override
public void handleEntry(SearchResultEntry entry) {
System.out.println("DN: " + entry.getDN());
for (String mail : entry.getAttributeStringValues("mail")) {
System.out.println(" mail: " + mail);
}
}
@Override
public void handleReference(String[] referralUrls) {
// Continuation references (RFC 4511 §4.5.3)
}
@Override
public void handleDone(LDAPResult result, LDAPSession session) {
if (!result.isSuccess()) {
System.err.println("Search failed: "
+ result.getDiagnosticMessage());
}
session.unbind();
}
});
| Scope | Description |
|---|---|
SearchScope.BASE | Search only the base entry |
SearchScope.ONE_LEVEL | Search immediate children of the base |
SearchScope.SUBTREE | Search the entire subtree rooted at the base |
Filters use the standard LDAP string representation. The encoder supports
equality (=), presence (=*), substring
(*), greater-or-equal (>=),
less-or-equal (<=), approximate match (~=,
RFC 4515 §4 / context tag 8), extensible match (:=,
RFC 4515 §4 / context tag 9), AND (&),
OR (|), and NOT (!).
Extensible match uses the syntax
[attr][:dn][:matchingRule]:=value.
The LDAPSession interface provides full CRUD operations
against the directory:
| Method | RFC 4511 | Description |
|---|---|---|
modify(dn, modifications, handler) |
§4.6 | Modify attributes of an existing entry |
add(dn, attributes, handler) |
§4.7 | Add a new entry |
delete(dn, handler) |
§4.8 | Delete an entry |
modifyDN(dn, newRDN, deleteOldRDN, handler) |
§4.9 | Rename or move an entry |
compare(dn, attribute, value, handler) |
§4.10 | Compare an attribute value |
extended(oid, value, handler) |
§4.12 | Extended operation (e.g. password modify, whoami) |
rebind(dn, password, handler) |
§4.2 | Re-bind with different credentials |
rebindSASL(mechanism, handler) |
§4.2 | Re-bind with a different SASL mechanism |
abandon(messageId) |
§4.11 | Cancel an in-progress operation |
The abandon(int messageId) method sends an AbandonRequest
(RFC 4511 §4.11) to cancel an in-progress operation, such as a
long-running search. The server is not required to honour the request and
does not send a response. Any pending callback for the abandoned
operation is removed immediately on the client side.
LDAP Controls (RFC 4511 §4.1.11) extend the behaviour of operations.
The Control class represents a control with an OID,
criticality flag, and optional value.
Call setRequestControls(List<Control>) before an
operation to attach controls. They are consumed (cleared) once the
next request is sent:
import org.bluezoo.gumdrop.ldap.client.Control; import java.util.Collections; // Paged results control (RFC 2696) byte[] pagedValue = encodePagedResultsControl(100, cookie); Control paged = new Control(Control.OID_PAGED_RESULTS, true, pagedValue); session.setRequestControls(Collections.singletonList(paged)); session.search(request, searchHandler);
After receiving a response, call getResponseControls()
on the session to retrieve any controls the server included.
Controls are also available on LDAPResult.getControls().
Unsolicited Notifications (RFC 4511 §4.4) are server-initiated
messages with messageID 0. The client automatically handles the
Notice of Disconnection (OID 1.3.6.1.4.1.1466.20036)
by logging the diagnostic message, notifying the handler via
onError(), and closing the connection.
IntermediateResponse (RFC 4511 §4.13) messages are delivered
to the pending callback for the corresponding operation, provided it
implements IntermediateResponseHandler. This enables
multi-stage extended operations to return incremental data.
Upgrade a plaintext connection to TLS using the STARTTLS extended operation (RFC 4511 §4.14, RFC 4513 §3). The client must have TLS credentials configured before connecting:
LDAPClient client = new LDAPClient(selectorLoop, "ldap.example.com", 389);
client.setKeystoreFile("/path/to/truststore.p12");
client.setKeystorePass("changeit");
client.connect(new LDAPConnectionReady() {
@Override
public void handleReady(LDAPConnected connection) {
connection.startTLS(new StartTLSResultHandler() {
@Override
public void handleTLSEstablished(LDAPPostTLS secureConn) {
// Now bind over TLS
secureConn.bind("cn=admin,dc=example,dc=com",
"secret", bindHandler);
}
@Override
public void handleStartTLSFailure(LDAPResult result,
LDAPConnected conn) {
System.err.println("STARTTLS failed: "
+ result.getDiagnosticMessage());
conn.unbind();
}
});
}
// ... lifecycle callbacks
});
For implicit TLS (LDAPS on port 636), enable secure mode before connecting:
LDAPClient client = new LDAPClient(selectorLoop, "ldap.example.com", 636);
client.setSecure(true);
client.setKeystoreFile("/path/to/truststore.p12");
client.setKeystorePass("changeit");
client.connect(handler);
| Method | Description |
|---|---|
setSecure(boolean) |
Enable implicit TLS (LDAPS) |
setSSLContext(SSLContext) |
Provide an externally-configured SSLContext |
setTrustManager(X509TrustManager) |
Custom trust manager for certificate verification |
setKeystoreFile(Path) |
Keystore for client certificate authentication |
setKeystorePass(String) |
Keystore password |
setKeystoreFormat(String) |
Keystore format (default: PKCS12) |
org.bluezoo.gumdrop.auth.LDAPRealm implements the framework's
Realm interface, enabling LDAP-backed authentication and
authorisation for any Gumdrop protocol (SMTP, IMAP, POP3, FTP, HTTP, etc.).
bind-dn/bind-password), or anonymously if no
service account is configureduser-filter (e.g.
(uid={0})) substituting the username
<realm id="ldapRealm" class="org.bluezoo.gumdrop.auth.LDAPRealm">
<property name="host">ldap.example.com</property>
<property name="port">389</property>
<property name="base-dn">dc=example,dc=com</property>
<property name="bind-dn">cn=service,dc=example,dc=com</property>
<property name="bind-password">secret</property>
<property name="user-filter">(uid={0})</property>
<property name="role-attribute">memberOf</property>
<property name="timeout">30</property>
</realm>
<realm id="ldapRealm" class="org.bluezoo.gumdrop.auth.LDAPRealm">
<property name="host">ldap.example.com</property>
<property name="port">636</property>
<property name="secure">true</property>
<property name="keystore-file">/etc/gumdrop/truststore.p12</property>
<property name="keystore-pass">changeit</property>
<property name="base-dn">dc=example,dc=com</property>
<property name="bind-dn">cn=service,dc=example,dc=com</property>
<property name="bind-password">secret</property>
<property name="user-filter">(uid={0})</property>
</realm>
To use SASL instead of simple bind for all LDAP operations (both service
account and user authentication), set sasl-mechanism:
<realm id="ldapRealm" class="org.bluezoo.gumdrop.auth.LDAPRealm">
<property name="host">ldap.example.com</property>
<property name="port">389</property>
<property name="sasl-mechanism">DIGEST-MD5</property>
<property name="base-dn">dc=example,dc=com</property>
<property name="bind-dn">cn=service,dc=example,dc=com</property>
<property name="bind-password">secret</property>
<property name="user-filter">(uid={0})</property>
</realm>
| Property | Type | Default | Description |
|---|---|---|---|
host |
String | localhost |
LDAP server hostname or IP address |
port |
int | 389 |
LDAP server port (636 for LDAPS) |
secure |
boolean | false |
Enable implicit TLS (LDAPS) |
start-tls |
boolean | false |
Upgrade to TLS via STARTTLS after connecting |
keystore-file |
Path | — | Path to the keystore/truststore for TLS |
keystore-pass |
String | — | Keystore password |
keystore-format |
String | PKCS12 |
Keystore format (PKCS12, JKS) |
base-dn |
String | "" |
Base DN for user searches (e.g. dc=example,dc=com) |
bind-dn |
String | — | Service account DN for searching. If empty, anonymous bind is used. |
bind-password |
String | — | Service account password |
user-filter |
String | (uid={0}) |
LDAP search filter template. {0} is replaced with the
username. Use (sAMAccountName={0}) for Active Directory. |
role-attribute |
String | memberOf |
Attribute containing group/role membership |
role-prefix |
String | "" |
Prefix prepended to role names before matching against
role-attribute values |
timeout |
int | 30 |
Operation timeout in seconds |
sasl-mechanism |
String | — | SASL mechanism for LDAP binds (PLAIN, CRAM-MD5, DIGEST-MD5, EXTERNAL, GSSAPI). When null, simple bind is used. |
cert-lookup-mode |
String | — | Certificate authentication mode: binary (match by DER)
or subject (extract CN from Subject DN) |
cert-username-attribute |
String | uid |
LDAP attribute to read as the username from a matched certificate entry |
cert-subject-filter |
String | — | Filter template for subject-mode certificate lookup.
Placeholders like {CN}, {O}, {OU}
are replaced with the corresponding RDN values from the certificate's
Subject DN. Example: (uid={CN}) |
When cert-lookup-mode is configured, the realm can authenticate
users by their TLS client certificate. This is used by protocol handlers that
support the SASL EXTERNAL mechanism (SMTP, IMAP, POP3) and by
the HTTP mTLS authentication provider.
Searches LDAP for an entry whose userCertificate;binary
attribute matches the DER encoding of the presented certificate:
<property name="cert-lookup-mode">binary</property> <property name="cert-username-attribute">uid</property>
Extracts RDN components from the certificate's Subject DN and substitutes them into a filter template:
<property name="cert-lookup-mode">subject</property>
<property name="cert-subject-filter">(uid={CN})</property>
<property name="cert-username-attribute">uid</property>
When using the LDAP client from within a Gumdrop server handler, create the client on the same SelectorLoop as the originating connection. This ensures that all callbacks execute on the same thread, avoiding synchronisation overhead:
SelectorLoop loop = endpoint.getSelectorLoop(); LDAPClient ldap = new LDAPClient(loop, "ldap.example.com", 389);
The LDAPRealm handles SelectorLoop affinity automatically via
its forSelectorLoop() method, which is called by each protocol
listener.
<realm id="ldapRealm" class="org.bluezoo.gumdrop.auth.LDAPRealm">
<property name="host">ldap.example.com</property>
<property name="port">636</property>
<property name="secure">true</property>
<property name="keystore-file">/etc/gumdrop/truststore.p12</property>
<property name="keystore-pass">changeit</property>
<property name="base-dn">dc=example,dc=com</property>
<property name="bind-dn">cn=smtp-service,dc=example,dc=com</property>
<property name="bind-password">secret</property>
<property name="user-filter">(mail={0})</property>
</realm>
<service id="smtp" class="org.bluezoo.gumdrop.smtp.SMTPService">
<property name="realm" ref="#ldapRealm"/>
<listener class="org.bluezoo.gumdrop.smtp.SMTPListener">
<property name="port">587</property>
<property name="secure">true</property>
<property name="keystore-file">/etc/gumdrop/keystore.p12</property>
<property name="keystore-pass">changeit</property>
</listener>
</service>
<realm id="adRealm" class="org.bluezoo.gumdrop.auth.LDAPRealm">
<property name="host">dc01.corp.example.com</property>
<property name="port">636</property>
<property name="secure">true</property>
<property name="keystore-file">/etc/gumdrop/ad-truststore.p12</property>
<property name="keystore-pass">changeit</property>
<property name="base-dn">dc=corp,dc=example,dc=com</property>
<property name="bind-dn">cn=svc-gumdrop,ou=Service Accounts,dc=corp,dc=example,dc=com</property>
<property name="bind-password">secret</property>
<property name="user-filter">(sAMAccountName={0})</property>
<property name="role-attribute">memberOf</property>
<property name="role-prefix">cn=</property>
</realm>
LDAPClient client = new LDAPClient(selectorLoop, "ldap.example.com", 389);
client.connect(new LDAPConnectionReady() {
@Override
public void handleReady(LDAPConnected connection) {
connection.bind("cn=admin,dc=example,dc=com", "secret",
new BindResultHandler() {
@Override
public void handleBindSuccess(LDAPSession session) {
// Add a new entry
Map<String, List<byte[]>> attrs = new HashMap<>();
attrs.put("objectClass", List.of(
"inetOrgPerson".getBytes(UTF_8),
"top".getBytes(UTF_8)));
attrs.put("cn", List.of("Alice".getBytes(UTF_8)));
attrs.put("sn", List.of("Smith".getBytes(UTF_8)));
attrs.put("mail", List.of(
"alice@example.com".getBytes(UTF_8)));
session.add(
"cn=Alice,ou=People,dc=example,dc=com",
attrs,
new AddResultHandler() {
@Override
public void handleAddResult(
LDAPResult result,
LDAPSession s) {
if (result.isSuccess()) {
System.out.println("Entry added");
} else {
System.err.println("Add failed: "
+ result.getDiagnosticMessage());
}
s.unbind();
}
});
}
@Override
public void handleBindFailure(LDAPResult result,
LDAPConnected conn) {
conn.unbind();
}
});
}
@Override
public void onConnected(Endpoint endpoint) { }
@Override
public void onSecurityEstablished(SecurityInfo info) { }
@Override
public void onError(Exception cause) {
cause.printStackTrace();
}
@Override
public void onDisconnected() { }
});
← Back to Main Page | Security | Redis Client | SMTP Server & Client
Gumdrop LDAP Client & Realm