Best practices
Follow these best practices for writing effective, maintainable, high-performance directory client applications.
Authenticate correctly
Unless your application performs only read operations, authenticate to the directory server. Some directory services require authentication to read directory data.
Once you authenticate (bind), directory servers make authorization decisions based on your identity. With servers that support proxied authorization, once authenticated, your application can request an operation on behalf of another identity, such as the identity of the end user.
Your application therefore should have an account, such as cn=My App,ou=Apps,dc=example,dc=com
.
The directory administrator can authorize appropriate access for your application’s account,
and monitor your application’s requests to help you troubleshoot problems if they arise.
Applications can use simple, password-based authentication. When using password-based authentication, use secure connections to protect credentials over the network. For applications, prefer certificate-based authentication if possible.
Reuse connections
LDAP is a stateful protocol. You authenticate (bind), you perform operations, you unbind. The server maintains a context that lets it make authorization decisions concerning your requests. Therefore, reuse connections whenever possible.
Because LDAP supports asynchronous requests, it is normal and expected to make multiple requests over the same connection. Your application can share a pool of connections to avoid the overhead of setting them up and tearing them down.
Check connection health
In a network built for HTTP applications, your long-lived LDAP connections can get cut by network equipment configured to treat idle and old connections as stale resources to reclaim.
When you maintain a particularly long-lived connection, such as a connection for a persistent search, periodically perform a health check to maintain the connection operational.
A health check involves reading or writing an attribute on a well-known entry in your data. It can serve the purposes of maintaining the connection operational, and of verifying access to your data. A success result for a read indicates that the data is available, and the application can read it. A success result for a write indicates that the data is available, and the application can write to it. The exact check to perform depends on how your application uses the directory. Under some circumstances, your data might be temporarily read-only, for example.
When using a connection timeout, take care not to set the timeout so low that long operations, such as unindexed searches, fail to complete before the timeout.
Request exactly what you need all at once
By the time your application makes it to production, you should know what attributes you want. Request them explicitly, and request all the attributes in the same search.
For example, if you require mail
and cn
, then specify both attributes in your search request.
Use specific LDAP filters
The difference in results between a general filter (mail=*@example.com)
,
and a good, specific filter like (mail=user@example.com)
can be
huge numbers of entries and enormous amounts of processing time,
both for the directory server that has to return search results,
and for your application that has to sort through them.
Many use cases can be handled with short, specific filters. As a rule, prefer equality filters over substring filters.
DS servers reject unindexed searches by default, because unindexed searches are resource-intensive. If your application needs to use a filter that results in an unindexed search, work with the directory administrator to find a solution, such as adding the indexes required for your search filters.
Always use &
with !
to restrict the potential result set
before returning all entries that do not match part of the filter.
For example, (&(location=Oslo)(!(mail=birthday.girl@example.com)))
.
Make modifications specific
Specific modifications help directory servers apply and replicate your changes more effectively.
When you modify attributes with multiple values, such as a static group member attribute, replace or delete specific values individually, rather than replacing the entire list of values.
Trust result codes
Trust the LDAP result code from the directory server. For example, if you request a modification, and you get a success result, consider the operation a success. Do not immediately issue a search to get the modified entry.
LDAP replication model is loosely convergent. In other words, the directory server sends you the success result before replicating the change to every directory server replica across the network. If you issue a read immediately after a write, a load balancer may direct the request to another replica. The result might differ from what you expect.
The loosely convergent model means that the entry could have changed since you read it. If needed, use LDAP assertions to set conditions for your LDAP operations.
Handle input securely
When taking input directly from a user or another program, use appropriate methods to sanitize the data. Failure to sanitize the input data can leave your application vulnerable to injection attacks.
For Java applications, the Directory Services format()
methods for filters and DNs
are similar to the Java String.format()
methods.
In addition to formatting the output, they escape the input objects.
When building a search filter, use one of the methods of the DS APIs to escape input.
Check group membership on the account, not the group
Reading an entire large static group entry to check membership is wasteful.
If you need to determine which groups an account belongs to, request the DS virtual attribute, isMemberOf
,
when you read the account entry.
Other directory servers use other names for this attribute that identifies the groups to an account belongs to.
Check support for features you use
Directory servers expose their capabilities as operational attribute values on the root DSE,
which is the entry whose DN is an empty string, ""
.
This lets your application discover capabilities at run time, rather than storing configuration separately. Putting effort into checking directory capabilities makes your application easier to deploy and to maintain.
For example, rather than hard-coding dc=example,dc=com
as a base DN in your configuration,
read the root DSE namingContexts
attribute.
Directory servers also expose their schema over LDAP.
The root DSE attribute subschemaSubentry
shows the DN of the entry for LDAP schema definitions.
Store large attribute values by reference
To serve results quickly with high availability, directory servers cache content and replicate it everywhere. If you already store large attribute values elsewhere, such as photos or audio messages, keep only a reference to external content in a user’s account.
Take care with persistent search and server-side sorting
A persistent search lets your application receive updates from the server as they happen by keeping the connection open and forcing the server to check whether to return additional results any time it performs a modification in the scope of your search. Directory administrators therefore might hesitate to grant persistent search access to your application.
DS servers expose a change log to let you discover updates with less overhead. If you do have to use a persistent search instead, try to narrow the scope of your search.
DS servers support a resource-intensive, standard operation called server-side sorting. When your application requests a server-side sort, the directory server retrieves all matching entries, sorts the entries in memory, and returns the results. For result sets of any size, server-side sorting ties up server resources that could be used elsewhere. Alternatives include sorting the results after your application receives them, or working with the directory administrator to enable appropriate browsing (virtual list view) indexes for applications that must regularly page through long lists of search results.
Reuse schemas where possible
DS servers come with schema definitions for a wide range of standard object classes and attribute types. Directories use unique, IANA-registered object identifiers (OIDs) to avoid object class and attribute type name clashes. The overall goal is Internet-wide interoperability.
Therefore, reuse schema definitions that already exist whenever you reasonably can. Reuse them as is. Do not try to redefine existing schema definitions.
If you must add schema definitions for your application, extend existing object classes with AUXILIARY classes. Take care to name your schemas such that they do not clash with other names.
When you have defined schema required for your application, work with the directory administrator to add your definitions to the directory service. DS servers let directory administrators update schema definitions over LDAP. There is no need to interrupt the service to add your application. Directory administrators can, however, have other reasons why they hesitate to add your schema definitions. Coming to the discussion prepared with good schema definitions, explanations of why they should be added, and evident regard for interoperability makes it easier for the directory administrator to grant your request.
Read directory server schemas during initialization
By default, Directory Services APIs use a minimal, built-in core schema, rather than reading the schema from the server. Doing so automatically would incur a significant performance cost. Unless schemas change, your application only needs to read them once.
When you start your application, read directory server schemas as a one-off initialization step.
Once you have the directory server schema definitions, use them to validate entries.
Handle referrals
When a directory server returns a search result, the result is not necessarily an entry. If the result is a referral, then your application should follow up with an additional search based on the URIs provided in the result.
Troubleshooting: check result codes
LDAP result codes are standard, and listed in LDAP result codes.
When your application receives a result, it must rely on the result code value to determine what action to take. When the result is not what you expect, read or at least log the additional message information.
Troubleshooting: check server logs
If you can read the directory server access log, then check what the server did with your application’s request.
The following excerpt shows a successful search by cn=My App,ou=Apps,dc=example,dc=com
:
Show excerpt
{"eventName":"DJ-LDAP","client":{"ip":"<clientIp>","port":12345},"server":{"ip":"<serverIp>","port":1636},"request":{"protocol":"LDAPS","operation":"CONNECT","connId":4},"transactionId":"0","response":{"status":"SUCCESSFUL","statusCode":"0","elapsedTime":0,"elapsedTimeUnits":"MILLISECONDS"},"timestamp":"<timestamp>","_id":"<uuid>"}
{"eventName":"DJ-LDAP","client":{"ip":"<clientIp>","port":12345},"server":{"ip":"<serverIp>","port":1636},"request":{"protocol":"LDAPS","operation":"TLS","connId":4},"transactionId":"0","response":{"status":"SUCCESSFUL","statusCode":"0","elapsedTime":0,"elapsedTimeUnits":"MILLISECONDS"},"security":{"protocol":"TLSv1.3","cipher":"TLS_AES_128_GCM_SHA256","ssf":128},"timestamp":"<timestamp>","_id":"<uuid>"}
{"eventName":"DJ-LDAP","client":{"ip":"<clientIp>","port":12345},"server":{"ip":"<serverIp>","port":1636},"request":{"protocol":"LDAPS","operation":"BIND","connId":4,"msgId":1,"version":"3","dn":"cn=My App,ou=Apps,dc=example,dc=com","authType":"SIMPLE"},"transactionId":"<uuid>","response":{"status":"SUCCESSFUL","statusCode":"0","elapsedTime":1,"elapsedQueueingTime":0,"elapsedProcessingTime":1,"elapsedTimeUnits":"MILLISECONDS","additionalItems":{"ssf":128}},"userId":"cn=My App,ou=Apps,dc=example,dc=com","timestamp":"<timestamp>","_id":"<uuid>"}
{"eventName":"DJ-LDAP","client":{"ip":"<clientIp>","port":12345},"server":{"ip":"<serverIp>","port":1636},"request":{"protocol":"LDAPS","operation":"SEARCH","connId":4,"msgId":2,"dn":"dc=example,dc=com","scope":"sub","filter":"(uid=kvaughan)","attrs":["isMemberOf"]},"transactionId":"<uuid>","response":{"status":"SUCCESSFUL","statusCode":"0","elapsedTime":3,"elapsedQueueingTime":0,"elapsedProcessingTime":3,"elapsedTimeUnits":"MILLISECONDS","nentries":1,"entrySize":430},"userId":"cn=My App,ou=Apps,dc=example,dc=com","timestamp":"<timestamp>","_id":"<uuid>"}
{"eventName":"DJ-LDAP","client":{"ip":"<clientIp>","port":12345},"server":{"ip":"<serverIp>","port":1636},"request":{"protocol":"LDAPS","operation":"UNBIND","connId":4,"msgId":3},"transactionId":"<uuid>","timestamp":"<timestamp>","_id":"<uuid>"}
{"eventName":"DJ-LDAP","client":{"ip":"<clientIp>","port":12345},"server":{"ip":"<serverIp>","port":1636},"request":{"protocol":"LDAPS","operation":"DISCONNECT","connId":4},"transactionId":"0","response":{"status":"SUCCESSFUL","statusCode":"0","elapsedTime":0,"elapsedTimeUnits":"MILLISECONDS","reason":"Client Unbind"},"timestamp":"<timestamp>","_id":"<uuid>"}
Notice these features of the messages:
-
The request operation types appear in upper case.
-
The messages track the client information and identify the specific sequence of operations with connection ID (
connId
) and message ID (msgID
) numbers. -
The
elapsedTime
for the response indicates the total time to complete the request. TheelapsedQueueingTime
is the time the request waited in the queue. TheelapsedProcessingTime
is the time actively processing the request. -
A status code 0 corresponds to a successful result, as described in RFC 4511.
For details about the messages format, refer to Common ForgeRock access logs.