ldapguru | blog

LDAP Programming Practices

Introduction

This page delivers information about known best practices for dealing with LDAP servers (directory servers) from a programming point-of-view (with some design pointers for good measure). The information is drawn primarily from experiences where people got it wrong and obtained puzzling or incorrect results, unexplained application processing failures, poor performance, and a sub-standard user experience. Following each piece of advice to the letter is very important and will save programmers much trouble, time, and frustration in the future. Never cut corners with programming, the easiest or fastest way is rarely the best way.

It would be difficult to say which of the best practices listed herein is the most important, but it could be said that matching rules, the root DSE, schema familiarity, understanding of the available request controls, features, and extensions, remembering that attribute values and DNs are un-ordered (that is, the ordering is not repeatable), and never, ever reading back an entry that was just modified have to be close to the top of the list.

Summary

general

authentication

passwords

schema

Prefer LDAP Version 3 in new code

LDAPv3 offers the following advantages over LDAPv2:

Do not use JNDI in new code

Java programmers should consider using the UnboundID LDAP SDK instead of JNDI for new code. The UnboundID LDAP SDK:

JNDI has very little to recommend it, the available examples are horrible, it uses a disconnected parameter setting mechanism, it has a number of defects, and it uses a deprecated configuration. The UnboundID LDAP SDK supports sensible constructs for connection and LDAP operations. The following class provides a method that transmits a simple bind request to a server and also sets a response timeout.

/*
 * Copyright 2008-2011 UnboundID Corp. All Rights Reserved.
 *
 * Copyright (C) 2008-2011 UnboundID Corp. This program is free
 * software; you can redistribute it and/or modify it under the terms of
 * the GNU General Public License (GPLv2 only) or the terms of the GNU
 * Lesser General Public License (LGPLv2.1 only) as published by the
 * Free Software Foundation. This program is distributed in the hope
 * that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
 * PURPOSE. See the GNU General Public License for more details. You
 * should have received a copy of the GNU General Public License along
 * with this program; if not, see http://www.gnu.org/licenses.
 */
package samplecode;


import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPConnectionOptions;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.util.Validator;


/**
 * Handles simple authentication on a connection to an LDAP server.
 */
@Author("terry.gardner@unboundid.com")
@Since("Dec 11, 2011")
@CodeVersion("1.0")
final class SimpleBindExample {


  BindResult authenticate(final LDAPConnection ldapConnection,final DN dn,
      final String password,final int responseTimeout) throws LDAPException {
    Validator.ensureNotNull(ldapConnection,dn,password);
    final SimpleBindRequest simpleBindRequest =
        new SimpleBindRequest(dn,password);
    final LDAPConnectionOptions connectionOptions = new LDAPConnectionOptions();
    connectionOptions.setResponseTimeoutMillis(responseTimeout);
    ldapConnection.setConnectionOptions(connectionOptions);
    BindResult result = ldapConnection.bind(simpleBindRequest);
    return result;
  }

}

The UnboundID LDAP SDK also provides packages that assist developers with migrating away from the Netscape LDAP SDK and JNDI.

Code to the published LDAP standard

Always write code that adheres to published LDAP standards. Avoid writing code that makes assumptions about the target directory server. If it is absolutely essential that features specific to a directory server be used, isolate the use of these features in the code. An example of this might be the use of the memberOf or isMemberOf attribute types. Another example would be the expectation that the server supports the numSubordinates attribute. Object-oriented programming languages make the process of encapsulating feature expectations a straightforward one.

Note that the LDAP standard expresses support for the vendorName and vendorVersion attributes the the Root DSE. Applications must not rely on the values contained in these attributes, and LDAP system administrators should configure access controls to prevent LDAP clients from retrieving these values.

Check with the Root DSE before using a request control or extension. If the OID does not appear in the Root DSE, do not use the control or extension.

Check the schema for attribute matching rules and do not, under any circumstances, perform bitwise or bytewise comparison of LDAP attribute values. Language comparison constructs such as == and = should not be used. AGAIN: applications must use matching rules to compare attribute values.

Understand and use the Root DSE

Consult the root DSE where necessary before making LDAP requests. Always check the root DSE before using an extension or request control. Read LDAP: The Root DSE for more information about the root DSE. The UnboundID LDAP SDK provides a class that encapsulates the root DSE, and a convenience methodLDAPConnection.getRootDSE().

Attributes that might be present in the root DSE:

Using the UnboundID LDAP SDK to check whether the OperationPurposeRequestControl is supported by the server:

LDAPConnectionPool ldapConnectionPool;
final boolean useOperationPurposeRequestControl;
try {
  final int initialConnections = 4;
  final int maxConnections = 6;
  ldapConnectionPool =
      new LDAPConnectionPool(getConnection(),
                             initialConnections,
                             maxConnections);

  /*
   * Determine whether the server supports the
   * OperationPurposeRequestControl:
   */
  final String controlOID =
      OperationPurposeRequestControl.OPERATION_PURPOSE_REQUEST_OID;
  useOperationPurposeRequestControl =
      ldapConnectionPool.getRootDSE().supportsControl(controlOID);
} catch (final LDAPException ldapException) {
  ldapException.printStackTrace();
  return null;
}

Prefer matching rules for comparisons

Code should use matching rules to make comparisons between attributes values, or between distinguished names. The directory server uses matching rules to match DNs and attribute values (for example, when matching values for fulfilling search requests), therefore, applications must use matching rules when comparing values. Failure to use matching rules can result in unexpected results.

By way of example, consider the following distinguished names:

'uid=user.0,ou=people,dc=example,dc=com'

and

'uid=user.0, ou=people, DC=example, DC=com'

These two DNs are equivalent, differing outwardly by case and spaces, but an application executing a character-by-character comparison would not consider the two to be the same (and would thus be incorrect). The use of matching rules prevents this error.

For further discussion and examples, see LDAP: Matching Rules.

Prefer connection pooling to individual connections

Applications should use connection pooling instead of individual connections for performance reasons and applications should re-use connections wherever possible to avoid the performance cost of establishing TCP/IP connections and secure connection handshakes. The UnboundID LDAP SDK provides an LDAPConnectionPool class which encapsulates connection pooling services. Application coders should determine, if necessary, the authorization state of a connection using the Authorization State Request Control or the Who am I? extended operation. See the example code.

Example of creating a connection pool using the UnboundID LDAP SDK:

LDAPConnectionPool connectionPool(final String hostname,
                                final int port,
                                final int initialConnections,
                                final int maxConnections)
  throws LDAPException {
    if(initialConnections <= 0) {
      throw new IllegalArgumentException(initialConnections +
          "is not a legal value for 'initialConnections'");
    }
   if(maxConnections <= 0) {
      throw new IllegalArgumentException(maxConnections +
          "is not a legal value for 'maxConnections'");
    }
    final LDAPConnection ldapConnection =
          new LDAPConnection(hostname,port);
    return new LDAPConnectionPool(ldapConnection,
                                  initialConnections,
                                  maxConnections);
}

Use application-specific accounts

Directory administrators and programmers should use application-specific authentication accounts instead of sharing accounts amongst applications, or even worse, using the rootDN. By contrast, application-specific authentication:

Do not rely on ordering of entries and attributes

Applications must not rely on the ordering of entries, attributes, and attribute values in a search request. See also LDAP: Attributes Are Not Ordered.

Entries, attributes, and attribute values can be returned in any order (that is, the ordering is not repeatable); entries, attributes, and attribute value ordering can change from one request to the next. Encapsulate search responses with listeners or callbacks to isolate attribute ordering variations from the main flow of the application code.

Check for controls in responses

Directory servers can attach response controls to most responses. A response control is a piece of data attached to a response. Always check for controls in a response and handle response controls with listeners or callbacks to isolate the processing of response controls from the application. Request controls should be listed as values for the supportedControl multi-valued attribute in the root DSE, but response controls need not be enumerated in the Root DSE. Programming APIs that do not support LDAP controls should probably not be used in non-trivial or mission-critical code.

Example of important response controls are password expired controls attached to bind responses and sort controls.

See also: The Root DSE

Use the post-read control instead of retrieving an entry after a modification

Always use the post-read control to verify updates instead of a search after an update. The post-read control is designed so that applications need not issue a search request after an update – it is bad form to retrieve an entry for the sole purpose of checking that an update worked because of the replication eventual consistency model. Never assume that your LDAP client connects to the same directory server for each request because architects may have placed load-balancers or LDAP proxies or both between LDAP clients and servers. Example:

  private void addDescriptionValues() {
    final List mods = new ArrayList();
    mods.add(new Modification(ModificationType.ADD,
                              "description",
                              "description 1"));
    mods.add(new Modification(ModificationType.ADD,
                              "description",
                              "description 2"));
    final ModifyRequest modifyRequest = new ModifyRequest(entry,mods);
    modifyRequest.addControl(new PreReadRequestControl("description"));
    modifyRequest.addControl(new PostReadRequestControl("description"));
    try {
      final LDAPResult result = ldapConnectionPool.modify(modifyRequest);
      final String preReadResponseControlOid =
          PreReadResponseControl.PRE_READ_RESPONSE_OID;
      if(result.hasResponseControl(preReadResponseControlOid)) {
        final Control c = result.getResponseControl(preReadResponseControlOid);
        final ReadOnlyEntry e = ((PreReadResponseControl)c).getEntry();
        System.out.println("the entry pre-modify:" + e);
      }
      final String postReadResponseOid =
          PostReadResponseControl.POST_READ_RESPONSE_OID;
      if(result.hasResponseControl(postReadResponseOid)) {
        final Control c = result.getResponseControl(postReadResponseOid);
        final ReadOnlyEntry e = ((PostReadResponseControl)c).getEntry();
        System.out.println("the entry post-modify:" + e);
      }
    } catch (final LDAPException lex) {
      lex.printStackTrace();
    }
  }

Passwords

If the directory server supports password-quality checks, passwords should be transmitted in clear text over a secure connection because the server will not be able to check password quality and history otherwise. Secure connections should be preferred for all LDAP interaction.

Directory Server security professionals should require that LDAP servers reject all requests except for the StartTLS request on unsecure connections.

See also authentication best practices.

Operational Attributes

Application code must not rely on the names or values of operational attributes, and applications must not attempt to modify the value of an operational attribute. Applications must not rely on being able to retrieve operational attributes from the directory – directory administrators can restrict access to operational attributes. Note that RFC4512 states that directory servers should maintain operational attributes like modifyTimestamp, not that they must maintain them. There are three different varieties of operational attributes:

References

LDAP Standards Documents at the IETF

Blog Entries

UnboundID

Miscellaneous

Last Update: 24-Feb–2013. © 2012–2013 Terry Gardner