Gumdrop

Gumdrop LDAP Client & Realm

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.

Contents

Architecture

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.

Key Components

Protocol State Machine

  connect()
     |
     v
 LDAPConnected  ──startTLS()──>  LDAPPostTLS
     |                                |
     ├── bind() ──────────────────────┤
     ├── bindSASL() ──────────────────┤
     └── bindAnonymous() ─────────────┘
                                      |
                                      v
                                 LDAPSession
                                      |
                      ┌───────────────┼───────────────┐
                      v               v               v
                  search()       modify()        rebind()
                  add()          delete()        rebindSASL()
                  compare()      modifyDN()      unbind()
                  extended()

Client Usage

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
    }
});

Connection Modes

ModeDefault PortDescription
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).

Binding (Authentication)

Simple Bind (RFC 4513 §5.1)

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);

Anonymous Bind

connection.bindAnonymous(bindHandler);

SASL Bind (RFC 4513 §5.2)

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.

Supported SASL Mechanisms

MechanismRFCDescription
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.

Usage

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();
    }
});

Search Scope

ScopeDescription
SearchScope.BASESearch only the base entry
SearchScope.ONE_LEVELSearch immediate children of the base
SearchScope.SUBTREESearch the entire subtree rooted at the base

Search Filters (RFC 4515)

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.

Directory Modification Operations

The LDAPSession interface provides full CRUD operations against the directory:

MethodRFC 4511Description
modify(dn, modifications, handler) §4.6Modify attributes of an existing entry
add(dn, attributes, handler) §4.7Add a new entry
delete(dn, handler) §4.8Delete an entry
modifyDN(dn, newRDN, deleteOldRDN, handler) §4.9Rename or move an entry
compare(dn, attribute, value, handler) §4.10Compare an attribute value
extended(oid, value, handler) §4.12Extended operation (e.g. password modify, whoami)
rebind(dn, password, handler) §4.2Re-bind with different credentials
rebindSASL(mechanism, handler) §4.2Re-bind with a different SASL mechanism
abandon(messageId) §4.11Cancel an in-progress operation

Abandon

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.

Controls

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.

Request Controls

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);

Response Controls

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 & Intermediate Responses

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.

STARTTLS

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
});

TLS / LDAPS

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);

Client Configuration Options

MethodDescription
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)

LDAPRealm

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.).

Authentication Flow

  1. Bind to the LDAP server using a service account (bind-dn/bind-password), or anonymously if no service account is configured
  2. Search for the user with user-filter (e.g. (uid={0})) substituting the username
  3. Re-bind as the discovered user DN with the user's password
  4. If the re-bind succeeds, the user is authenticated

Configuration Example

<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>

LDAPS Configuration

<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>

SASL Bind Configuration

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>

LDAPRealm Configuration Reference

PropertyTypeDefaultDescription
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})

Certificate Authentication

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.

Binary Mode

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>

Subject DN Mode

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>

SelectorLoop Affinity

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.

Integration Examples

Using LDAPRealm with SMTP

<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>

Active Directory Configuration

<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>

Programmatic LDAP Client Usage

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