Ping Identity Directory Server versions 7.2.1.1 and 7.0.1.3

Ping Identity Directory Server versions 7.2.1.1 and 7.0.1.3 have been released. These are security updates, and customers running 7.x versions are strongly encouraged to upgrade.

The most important update included in these releases is a fix for a critical security issue introduced in the 7.0.0.0 version that could cause certain passwords to be recorded in the clear on the server filesystem. There are two instances in which this could have occurred:

  • When creating an encrypted backup of the alarms, alerts, configuration, encryption settings, schema, tasks, or trust store backends, the backup descriptor was supposed to include the identifier of the encryption settings definition that was used to protect the contents of the backup. Instead of this identifier, the server would incorrectly include the password that backed that encryption settings definition. This issue did not affect backups of local DB backends (like userRoot), the LDAP-accessible changelog, or the replication database.

  • The server maintains a tool invocation log (logs/tools/tool-invocation.log), which keeps track of certain commands that are run on the system, especially those that may be used to alter the server configuration or data. Among other things, this tool includes the name of the tool and the arguments used to run it. Sensitive arguments, like those used to provide passwords, should automatically be redacted. However, if the tool is run with an argument that provides the path to a file containing a password, a bug could have caused the tool invocation log to record the contents of the first line of that file (which usually contains the password itself) rather than the path to that file. The following command-line tools were affected by this issue:

    • backup
    • create-initial-config
    • create-initial-proxy-config
    • dsreplication
    • enter-lockdown-mode
    • export-ldif
    • import-ldif
    • ldappasswordmodify
    • leave-lockdown-mode
    • manage-tasks
    • manage-topology
    • migrate-ldap-schema
    • parallel-update
    • prepare-endpoint-server
    • prepare-external-server
    • realtime-sync
    • rebuild-index
    • re-encode-entries
    • reload-http-connection-handler-certificates
    • reload-index
    • remove-defunct-server
    • restore
    • rotate-log
    • stop-server

Other tools were not affected by this second issue. Also note that this issue only involved passwords provided in files that were directly referenced as arguments on the command line. Passwords that were provided directly on the command line, and passwords that were automatically included because of their presence in a tools.properties file, were properly redacted. Because of the nature of this issue, regular user passwords are not likely to have been exposed, but the passwords of administrators that may have run commands on the server system could have been recorded.

In both issues above, the passwords were written to a file on the server filesystem with permissions that made them only accessible to the account used to run the server. Other accounts on the system should not have been able to read the contents of those files. Nevertheless, if you believe that any passwords may have been compromised, we recommend taking the following steps to mitigate the risk:

  1. Update the server to a version that includes the fix for this issue. If you’re running version a 7.2 version, then you should upgrade to the 7.2.1.1 release. If you’re running a 7.0 version, then you should upgrade to either version 7.2.1.1 or version 7.0.1.3.
  2. If you believe that any user passwords may have been exposed in the logs/tools/tool-invocation.log file, then change the passwords for those users and sanitize or delete that log file.
  3. If you believe that an encryption settings definition password may have been exposed in a backup descriptor, then create a new encryption settings definition, set it as the preferred definition for all subsequent encryption operations, export your data to LDIF, and re-import the data so that it is re-encrypted with the new definition. Create new backups, and destroy old backups with the compromised password.

In addition to fixing the bugs that led to the potential exposure of these passwords, we have added additional automated tests to help ensure that other problems like this do not occur in the future.

Other Changes Included in the 7.2.1.1 Release

The following additional fixes have been included in the 7.2.1.1 release:

  • Updated the behavior that the server exhibits if an attribute type is removed from the schema while that attribute type is still referenced by one or more server backends. In earlier releases, the server could fail to open a backend that referenced an attribute type that is no longer defined in the schema. The server will now permit the backend to be opened, but will generate an alert about any missing attribute type definitions on startup, and will also generate an alert on any access to an entry that contains a reference to a missing attribute type. The server will also attempt to prevent the removal of an attribute type that is still referenced by any of the backends.
  • Fixed an issue in which the stop-server.bat batch file may not function properly on Windows systems with a locale that uses a character other than a period as a decimal separator.
  • Fixed an issue in which the periodic stats logger output could have been difficult to parse on systems with a locale that uses a character other than the period as a decimal separator.
  • Fixed an issue that prevented creating a constructed virtual attribute for an attribute that was marked SINGLE-VALUE in the server schema.
  • Fixed an issue in which backups of the server’s encryption settings database could have been (automatically or explicitly) encrypted with a key from the encryption settings database.

Other Changes Included in the 7.0.1.3 Release

The following additional fixes have been included in the 7.0.1.3 release:

  • Added debug logging for DNS lookups that take longer than a configured length of time (10 seconds by default). A new “DNS Resolution” monitor entry is available to provide information about DNS lookups performed by the server.
  • Fixed an issue in which SCIM searches could have an incorrect startIndex value if the scim-resources.xml file was configured with multiple base DNs.
  • Fixed an issue that could cause an error while performing an encrypted LDIF export of a directory with a very large number of non-leaf entries. In such cases, the LDIF export will be split into multiple files, but the attempt to merge those files at the end of processing would fail. This error would not result in any data loss or exposure, and the exported data could still be imported by either providing all of the files to the import-ldif utility with separate –ldifFile arguments or by manually merging the files.

Naming Entries With entryUUID in the Ping Identity Directory Server

Choosing an entry’s RDN is something that shouldn’t be taken lightly. Ideally, it should meet all of the following criteria:

  • It needs to be unique so that it doesn’t conflict with the RDNs of any other entries beneath the same parent.
  • It should be something that’s not likely to change so that clients don’t have to worry about performing modify DN operations.
  • It should be something that doesn’t contain any personally identifiable or otherwise sensitive information. DNs are often included in log messages, and if a client has permission to see any part of an entry, then they’ll be able to see its DN.
  • It shouldn’t be something predictable. An attacker shouldn’t be able to guess the DN of a specific user, or even of any user in the server.

This means that things like usernames, common names, email addresses, and telephone numbers aren’t good choices. Account numbers are also not great because they tend to follow predictable patterns (e.g., sequentially increasing numbers).

What you really want is something that is basically random and has enough entropy to ensure that you won’t get an accidental conflict and so that an attacker will be unlikely to guess a valid value. It would be easy enough for a client to generate a long-ish random string to use for this purpose, but it turns out that the directory server (at least, a server that supports RFC 4530) already generates just such a value for each entry: its entryUUID.

Of course, there’s a catch-22 problem with using the entryUUID attribute as the naming attribute for an entry: the client doesn’t know what the entryUUID is going to be because it’s generated by the server. The client can’t specify it because the entryUUID attribute type is declared with the NO-USER-MODIFICATION constraint.

One potential workaround would be to create an entry with a throwaway value for the RDN, figure out what the entry’s entryUUID value is (using either the post-read control or by issuing a search to retrieve the entry), and issue a modify DN operation to rename the entry using that value. But that’s a hassle, and it puts undue burden on both the client and the server. Fortunately, if you’re using the Ping Identity Directory Server, then you have a couple of additional options:

  • The client can include the “name with entryUUID” request control in the add request.
  • The server can be configured so that any add request matching a specified set of criteria automatically gets created with entryUUID as its naming attribute.

Each of these will be described in more detail below.

The Name With entryUUID Request Control

The name with entryUUID request control may be included in an add request to indicate that the server should replace the RDN with the provided entry with one that uses the name and value of the entryUUID attribute the server generated for the entry. This control has an OID of “1.3.6.1.4.1.30221.2.5.44” and no value. We recommend that it be marked critical so that the add attempt will fail if the server cannot honor the request.

When using this control, the client should supply a DN for the entry that indicates the location in the DIT where the new entry should reside, but the RDN for the DN doesn’t really matter because it’s going to get replaced with the entryUUID. If you want, you can use an attribute value from the entry to add (just like if you were adding the entry without the control), but you can also use a bogus name-value pair. For example, you could provide a DN of “replaceWithEntryUUID=replaceWithEntryUUID,ou=People,dc=example,dc=com”, and the server would add the entry with a DN like “entryUUID=4869eea6-90bf-45bf-9fcb-eac096564bc8,ou=People,dc=example,dc=com” (although of course the entryUUID would vary each time).

Of course, there is one big issue with using this control: when the entry is added, the client won’t know what the entry’s actual DN really is. The way that we address that is to treat an add request that includes the name with entryUUID request control as if it also included a post-read request control with a single requested attribute of entryUUID. This will cause the add response to include a post-read response control with the DN and entryUUID value for the entry that was added. If you want additional attributes from the entry, you can explicitly include a post-read request control along with the name with entryUUID request control in the add request with the attributes you want to retrieve.

We provide support for the name with entryUUID request control in the ldapmodify command-line tool through the --nameWithEntryUIUD argument. For example:

$ bin/ldapmodify --hostname ds.example.com \
     --port 636 \
     --useSSL \
     --bindDN "cn=Name With entryUUID Example,ou=Applications,dc=example,dc=com" \
     --nameWithEntryUUID
Enter the bind password:

The server presented the following certificate chain:

     Subject: CN=ds.example.com,O=Ping Identity Self-Signed Certificate
     Valid From: Saturday, April 27, 2019 at 11:11:58 AM CDT
     Valid Until: Saturday, April 23, 2039 at 11:11:58 AM CDT
     SHA-1 Fingerprint: 41:5f:72:4a:e0:d0:22:18:3e:59:90:6f:65:fc:fe:34:f1:39:84:68
     256-bit SHA-2 Fingerprint: 54:d5:58:07:bd:af:8b:b4:19:8e:03:a3:c5:14:0d:2a:e6:1e:c2:3a:29:6c:17:5f:5f:61:97:1d:31:3d:2b:ac

WARNING:  The certificate is self-signed.

Do you wish to trust this certificate?  Enter 'y' or 'n': y
# Successfully connected to ds.example.com:636.

dn: replaceWithEntryUUID=replaceWithEntryUUID,ou=People,dc=example,dc=com
changetype: add
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
uid: test.user
givenName: Test
sn: User
cn: Test User
userPassword: testUserPassword

# Adding entry
# replaceWithEntryUUID=replaceWithEntryUUID,ou=People,dc=example,dc=com ...
# Result Code:  0 (success)
# Post-Read Response Control:
#      OID:  1.3.6.1.1.13.2
#      Post-Read Entry:
#           dn: entryUUID=7866e6d4-faa7-40e4-bad0-9ef26e566efd,ou=People,dc=exa
#            mple,dc=com
#           entryUUID: 7866e6d4-faa7-40e4-bad0-9ef26e566efd

Since the control doesn’t have a value, it’s easy enough to use in any LDAP API that supports controls (although you may find it a chore to get the DN of the resulting entry if that API doesn’t also support the post-read response control). But if you’re using the UnboundID LDAP SDK for Java, we provide direct support for the control through the NameWithEntryUUIDRequestControl class. I’ve written a simple AddEntryNamedWithUUID program to demonstrate how to use this class to add an entry with the request control and get its DN.

Automatically Naming Entries With entryUUID

Although it’s pretty simple to use the control in an add request to explicitly indicate that an entry should use entryUUID as the naming attribute, this does require the client to know about and use the control. This isn’t always possible, but the Ping Identity Directory Server has you covered there as well. You can configure the server so that any add request that matches a specified set of criteria will automatically be treated as if it included the name with entryUUID request control. This option is available through the following pair of properties in the global configuration:

  • auto-name-with-entry-uuid-connection-criteria
  • auto-name-with-entry-uuid-request-criteria

For example, if you wanted to configure the server so that any entry added with the “person” object class will behave as if it included the name with entryUUID request control, you would use a configuration like the following:

dsconfig create-request-criteria \
     --criteria-name "Adds of Person Entries" \
     --type simple \
     --set operation-type:add \
     --set "any-included-target-entry-filter:(objectClass=person)"

dsconfig set-global-configuration-prop \
     --set "auto-name-with-entry-uuid-request-criteria:Adds of Person Entries"

At this point, adding an entry with the “person” object class from any client will cause that entry’s RDN to be replaced with one generated based on the entryUUID operational attribute. The response will include the post-read response control as if the request had included the name with entryUUID request control (although the client will likely not know to look for it).

Ping Identity Directory Server 7.2.1.0

We have just released the Ping Identity Directory Server version 7.2.1.0, available for download at https://www.pingidentity.com/en/resources/downloads/pingdirectory-downloads.html. This is primarily a bugfix release, but it does offer a couple of significant new features. The release notes provide a pretty comprehensive overview of the changes, but the most significant updates are:

  • Fixed an issue that could cause an error during an LDIF export of a data set with a large number of non-leaf entries. In such cases, the LDIF data may be split into multiple files to make the LDIF process faster. If the data is split into multiple files, and if the LDIF export was encrypted, then an error may have prevented merging those files at the end of the export process. The exported data was still valid and could still be successfully imported, but with additional effort required.
  • Updated the LDAP pass-through authentication plugin to add an option to construct the DN to use to authenticate to the remote server from information in the local entry. Further, it is now possible to authenticate to the remote server with a bind DN value that may not be a valid LDAP distinguished name (for example, using the user principal name when passing through authentication to an Active Directory server).
  • Updated the LDAP pass-through authentication to add an included-local-entry-base-dn configuration property that makes it easier to identify which local users for which pass-through authentication may be attempted. If pass-through authentication is enabled, it will no longer be attempted by default for root users or topology administrators.
  • Fixed a number of issues in the LDAP pass-through authentication plugin. It will now use separate connections for search and bind operations. It will now make better use of multiple servers for improved availability, and can re-try a failed operation when only a single server is configured. Improved the troubleshooting information that is available when a problem is encountered during pass-through authentication processing.
  • Fixed an issue that could cause entryUUID mismatches across servers if the server is configured to automatically use entryUUD as the naming attribute for entries matching a given set of criteria.
  • Updated the server to ensure that information about missing replication changes persistent across restarts. If the server has been offline for longer than the replication purge delay, then replication will be unable to automatically bring that server back in sync with the other servers in the topology. However, if the server had been restarted after that problem was identified, the record of the missing changes could be inadvertently cleared.
  • Updated the dsreplication tool to allow enabling replication on a node whose topology information is out of sync with the topology master.
  • Updated the topology manager to make it easier to diagnose connection errors between servers in the topology.
  • Added logging for DNS lookups that take longer than expected to complete (10 seconds by default). This can make it easier to identify problems with DNS issues cause connectivity problems or slowness.
  • The delegated administration configuration has changed significantly. When updating an existing installation, the update tool will automatically convert the old configuration model to the new one.
  • The Data Synchronization Server has been updated to support bidirectional synchronization with the PingOne for Customers hosted directory service. The 7.2.0.0 release added support for the PingOne for Customers service as a sync destination. With the 7.2.1.0 release, it is now also possible to use PingOne for Customers as a sync source.

So I guess SLAMD is a thing again…

The year was 2002. I had recently jumped ship from Netscape to Sun Microsystems after AOL bought Netscape and decided they wanted out of their iPlanet alliance. I was working as a sustaining engineer on whatever Sun’s brilliant marketeers decided to call their LDAP directory server at the time. One day, my boss, Steve Shoaff, came into my office with a couple of ideas. He said that he wanted me to build a tool that could measure the directory server performance with a lot of load by hitting it from multiple clients at the same time. And he said that he wanted to call it “SLAMD”, which is a play on “slapd”, which kind of stands for “standalone LDAP daemon” and is used in the process names of some directory server products.

So I built it, and I think that it’s fair to say that it turned into something substantially more impressive than either of us originally imagined. It had a Java-based API that you could use to define the types of workloads that you wanted to process, a web-based interface that you could use to schedule jobs and view the results (numerically and graphically), and a client that you could install on the systems that you wanted to used to drive load against the server. Over time, I added new types of jobs and lots of other features, like self-optimizing jobs (which repeatedly run the same job with different amounts of client load to find the optimal performance), job groups (which let you schedule several jobs to run in succession), and resource monitoring (which lets you monitor system statistics like CPU, disk, and network utilization on various systems).

SLAMD was pretty good at what it did, and it worked with all types of LDAP-compliant directory servers, so it became one of the preeminent directory server benchmarking tools. We convinced Sun to open source it, and lots of people started using it. It could be used for things other than directory servers, too (I did build some basic support for other protocols like HTTP, POP3, IMAP, and SMTP, and the ability to interact with relational databases), but LDAP performance and stress testing was always its big wheelhouse.

Fast forward several years, and SLAMD was still pretty great but was starting to show its age, at least under the covers. I started working on it in the Java 1.3 days, before nice features like generics, foreach, concurrency APIs, sub-millisecond timing, and so much more. The web interface was all hand-crafted HTML and mostly contained in one giant source file, and it was getting pretty unwieldy. I did make some attempts to try to modernize it, but I got really busy with other things, like creating OpenDS as a replacement for Sun’s stagnating C-based directory server, then moving on from Sun to launch UnboundID and working furiously to build up its directory server, directory proxy server, and LDAP SDK products.

Shortly after Sun and I parted ways, Oracle bought Sun and gradually started killing off most of the good things about it. This included shutting down the java.net site, which had been the open source repository for SLAMD, and I decided to take that opportunity to just let it kind of fade away. I figured it might be better to start something new from scratch, with a much more modern foundation, than to try to give SLAMD the kind of makeover I thought it needed. Of course, that was nearly a decade ago, and while I’ve done a lot since then, creating a new directory server benchmarking tool (other than a handful of command-line tools like searchrate and modrate that we ship with the LDAP SDK) hasn’t really been in the cards. Meanwhile, SLAMD is still getting a surprising amount of use. Even though it’s not so easy to get your hands on it anymore, people were still getting their hands on it and using it.

After having the topic come up several times in the last few weeks, I finally bit the bullet and dusted off the old code. I spent a couple of weekends doing some pretty extensive code cleanup. I fully generified everything, so there aren’t any more build warnings about raw types. I pulled in much more modern versions of dependencies like Apache Tomcat, the Berkeley DB Java Edition, and the UnboundID LDAP SDK for Java. I reorganized some of the jobs, including putting some legacy stuff out to pasture, and I wrote new versions of several of them. I split up some of the admin interface code into separate source files to make it more manageable, and I made some minor user interface enhancements.

So anyway, I went ahead and put the updated code on GitHub at https://github.com/dirmgr/slamd. Since no single entity owns the copyright on the code, it’s not possible to change the license, and it will therefore always will be licensed under the terms of the Sun Public License version 1.0. I’m not promising that I’ll add any major new features, but it’ll at least be more readily available than it has been, and with some more modern guts.

For now, if you want to use it, you’ll need to check it out and build it for yourself (there’s a README that tells you how to do that). Just know that it’s not backward-compatible with the version that I last touched in 2010, so don’t try to upgrade an existing instance (but if you do want the code for that old version, just check out revision 5777f3e5d78ff03985af4e68670e649127339c59, since I used it to seed the new repository).

Also note that there’s still a lot more work to do. There’s quite a bit more code cleanup that’s still on my to-do list (it builds cleanly with Java 8, but there are several deprecation warnings with Java 11). I plan on rewriting some more of the jobs (including making some potentially-incompatible changes). I know that some of the resource monitoring is broken (at least on Linux, which isn’t so concerned about maintaining consistent output in some of its commands). I haven’t touched any of the documentation. I’ve only done a very minimal amount of testing so far. So while it’s fine to play around with what’s there now, and please report issues if you find them, just know that I reserve the right to make even more non-backward-compatible changes as I continue to modernize the code.

UnboundID LDAP SDK for Java 4.0.10

We have just released version 4.0.10 of the UnboundID LDAP SDK for Java. It is available for download from the releases page of our GitHub repository, from the Files page of our SourceForge repository, and from the Maven Central Repository.

By the way, this is the first release that has been built from the public GitHub repository. All previous releases were built from an internal subversion repository that had been kept in sync with the GitHub repository. The only visible evidence of this change should be in the com.unboundid.ldap.sdk.Version class, where the REVISION_NUMBER constant (which has been deprecated for a couple of years) now has an integer value of -1 instead of the subversion revision number, and the REVISION_ID constant (which is the preferred replacement for REVISION_NUMBER) now reflects the GitHub commit digest (“b2272901fd62ad978017ff1aeb049cafc1999b12” for the 4.0.10 release) instead of the internal subversion revision number.

The most significant changes included in this release are:

  • Fixed a bug in generating the normalized string representation of an RDN with multiple values that have the same attribute type (for example, “cn=foo+cn=bar”). In such cases, the normalized representation would only have contained one value with that attribute type, and any other values with the same attribute type would have been incorrectly omitted. Further, because the normalized string representation of an RDN is used for other purposes (for example, determining equality and comparator ordering), this may fix other related issues as well.
  • Added methods for improved DN and RDN validation that make it possible to require attribute names to strictly comply with the requirements of the LDAP specification. Previously, the methods for creating and validating DNs and RDNs were always lenient with what they would allow (for example, allowing attribute names with underscores) since some servers are lenient in this regard. The existing methods are still lenient by default for the sake of backward compatibility, but there is now an option to require strict compliance with the specification.
  • Improved support for TLS version 1.3 in JVMs that support it (which should be Java 11 and higher). The LDAP SDK will now automatically enable support for TLSv1.3 if it is available, and will prefer that protocol if the server also supports it, but it can still fall back to an earlier protocol version (TLSv1.2, TLSv1.1, or TLSv1, whichever is the highest version that the server supports) if necessary. As before, the default set of TLS protocols can be overridden programmatically by calling methods in the com.unboundid.util.SSLUtil class or by setting system properties.
  • Updated the process for establishing a secure connection so that it immediately starts the TLS handshake on the socket, rather than waiting for it to happen on the first attempt to communicate over the connection. This can help ensure that the connection is ready to use more quickly, and can help avoid timing issues in certain cases where the prompt trust manager is used in interactive applications that may prompt for other user input.
  • Updated the in-memory-directory-server command-line tool to add support for a number of new arguments, including --generateSelfSignedCertificate, --maxConcurrentConnections, --sizeLimit, --passwordAttribute, --defaultPasswordEncoding, --allowedOperationType, and --authenticationRequiredOperationType.
  • Updated the ldap-debugger tool to add a --generateSelfSignedCertificate argument. If the tool is configured to listen using SSL, then this argument can be given as an alternative to the --keyStorePath argument to indicate that the tool should generate its own self-signed certificate instead of requiring the user to supply a certificate.
  • Updated the ResultCode.isConnectionUsable method so that UNWILLING_TO_PERFORM is no longer included in the set of result codes that will cause the LDAP SDK to suspect that the connection may no longer be usable. Although it is possible that the connection may have become invalid, there are plenty of reasons that an LDAP server may return an UNWILLING_TO_RETURN response for a connection that remains completely usable. Since isConnectionUsable is often used to decide whether to keep the existing connection or throw it away and replace it with a new one, being too prone to indicate that a connection is no longer usable can adversely impact application performance and increase load on the directory server.
  • Added a new API that can be used to change the way that the LDAP SDK resolves names to IP addresses, and IP addresses to names. The default implementation simply uses the JVM’s standard name resolution methods, but a caching name resolver implementation is also provided that can offer better performance and better resilience against name service outages.
  • Added a new PasswordFileReader class that makes it easier to read a password from a file. The password files may optionally be gzip-compressed and/or passphrase-encrypted, and the reader validates that the file contains exactly one line and that the line is non-empty. All command-line tools now have access to a password file reader, and LDAP SDK tools that can read passwords from files have been updated to take advantage of it.
  • Updated the command-line tool framework so that tools that support reading argument values from properties files can now handle the case in which the properties file is gzip-compressed and/or passphrase-encrypted.
  • Fixed a potential null pointer exception in ArgumentParser.toString that could arise if the parser was created through serialization and there were not any additional description paragraphs. Also, eliminated an unnecessary quotation mark in the generated string representation.
  • Updated the ldapsearch and ldapmodify command-line tools to add support for the get backend set ID and get server ID request controls (which can be used to obtain information from a Ping Identity Directory Server or Ping Identity Directory Proxy Server about which entry-balancing sets or which server instances were used to process a request), and for the route to backend set and route to server request controls (which can be used to request that the Ping Identity Directory Proxy Server route the request to a specific group of entry-balancing backend sets or to a specific backend server).
  • Updated LDAP command-line tools to support authentication with the UNBOUNDID-CERTIFICATE-PLUS-PASSWORD SASL mechanism.
  • Added StaticUtils convenience methods for creating maps and sets with predefined sets of elements.
  • Updated the LDIF writer to make its user-friendly display of base64-encoded values more filter-friendly. The LDIF writer has a feature that allows it to automatically include a comment below a base64-encoded value that tries to display a more human-readable version of that value, but with special characters escaped. In most cases, that more human-readable value could have been directly copied into the string representation of a search filter, but there were previously some cases where that was not true (for example, cases where the raw value included parentheses, an asterisk, a horizontal tab, a carriage return, or a line feed).
  • Updated the UniquenessResponseControl class to add convenience methods to help make it easier to interpret the response. Updated the UniquenessRequestControl class to add an example to the class-level Javadoc documentation.

Password Retirement in the Ping Identity Directory Server

Changing the password for an account stored in an LDAP directory server can sometimes be a race against time, especially for accounts that are used by applications. For example, let’s say that you’ve got a web application that uses a directory server to authenticate users and store their profile information. That application probably has its own account that it uses to authenticate to the directory server, and you’ve probably got several instances of that same application running on different servers all sharing that same account. If you need to change the password for that application account, then you risk breaking any instances of the application that need to authenticate to the server between the time that you change the password and the time that you can update the application with the new password.

It would be nice if there were some kind of grace period around password changes, in which the new password is immediately available to use, but the old password still works for a limited period of time. It just so happens that the Ping Identity Directory Server provides this capability through a feature that we call password retirement. It’s disabled by default, but you can enable it by adding one or more values for the password-retirement-behavior property in the password policy that governs the desired user account. The allowed values for this property are:

  • retire-on-self-change — Indicates that the server should automatically retire a user’s previous password whenever they change their own password.
  • retire-on-administrative-reset — Indicates that the server should automatically retire a user’s previous password whenever an administrator resets their password.
  • retire-on-request-with-control — Indicates that the server should retire a user’s previous password whenever the operation used to change the password includes the retire password request control.

The password policy also offers a max-retired-password-age configuration property, which specifies the length of time that a retired password should be considered valid.

As an example, let’s say that you want to enable automatic password retirement whenever a user changes their own password and when a client issues a request that includes the retire password request control, and you want the previous password to remain valid for one hour. If you want to make that change in the default password policy, the command to do that would be:

dsconfig set-password-policy-prop \
     --policy-name "Default Password Policy" \
     --set password-retirement-behavior:retire-on-self-change \
     --set password-retirement-behavior:retire-on-request-with-control \
     --set "max-retired-password-age:1 h"

Note that if you successfully authenticate with a retired password, the server will include the password expiring request control (as described in draft-vchu-ldap-pwd-policy-00.txt) in the bind response. This response control indicates that the password is only valid for a limited period of time, and its value specifies the number of seconds that the password will remain valid.

The Retire Password Request Control

The retire password request control can be included in either an LDAP modify request or in a password modify extended request. It explicitly indicates that the server should retire the user’s old password so that it can continue to be used for a limited period of time. The control has an OID of “1.3.6.1.4.1.30221.2.5.31”, and it does not take a value. The UnboundID LDAP SDK for Java provides support for this control via the RetirePasswordRequestControl class, but since it doesn’t require a value, it’s easy to use in any other LDAP API using just the OID.

For the server to honor the retire password request control, the target user’s password policy does need to be configured with retire-on-request-with-control as one of the values for the password-retirement-behavior property. If the password policy’s retirement behavior would have automatically retired the former password anyway, then including the retire password request control in the request used to change the password isn’t necessary, but it won’t hurt anything.

The Purge Password Request Control

The purge password request control can also be included in either an LDAP modify request or in a password modify extended request. It explicitly indicates that the server should purge the user’s former password when setting a new one. This can be useful, for example, if you suspect that the user’s password might have been compromised and you don’t want to allow it to be used after the password change. The purge password request control has an OID of “1.3.6.1.4.1.30221.2.5.32”, and it does not require a value. The UnboundID LDAP SDK for Java provides support for this control through the PurgePasswordRequestControl class, but it’s easy to use the control in other LDAP APIs with just the request OID.

If it is present in a request, then the purge password request control will override the password-retirement-behavior configuration in the password policy. You can use it to ensure that the former password won’t be retired, even if the server would have automatically retired the password without this control.

Using the Retire and Purge Password Controls With ldapmodify or ldappasswordmodify

Both the ldapmodify tool (which allows for requesting add, delete, modify, and modify DN operations) and the ldappasswordmodify tool (which allows for requesting the password modify extended operation) support both the retire password request control and the purge password request control. The controls can be included in applicable requests using the --retireCurrentPassword or --purgeCurrentPassword arguments, respectively.

For example, let’s say that the user “uid=jdoe,ou=People,dc=example,dc=com” currently has a password of “originalPassword”. If we want to use an LDAP modify operation to perform a self-password change to make it “secondPassword”, and if we want to include the retire password request control in the modify request, then we can do that with the following command:

$ bin/ldapmodify --hostname ldap.example.com \
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=jdoe,ou=People,dc=example,dc=com" \
     --bindPassword originalPassword \
     --retireCurrentPassword
# Successfully connected to ldap.example.com:636.

dn: uid=jdoe,ou=People,dc=example,dc=com
changetype: modify
delete: userPassword
userPassword: originalPassword
-
add: userPassword
userPassword: secondPassword
-

# Modifying entry uid=jdoe,ou=People,dc=example,dc=com ...
# Result Code:  0 (success)

We can use the ldapsearch tool to verify that the user can now use either the new password or the former password to authenticate:

$ bin/ldapsearch --hostname ldap.example.com \
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=jdoe,ou=People,dc=example,dc=com" \
     --bindPassword secondPassword \
     --baseDN "dc=example,dc=com" \
     --scope base \
     "(objectClass=*)"
dn: dc=example,dc=com
objectClass: top
objectClass: domain
dc: example

# Result Code:  0 (success)
# Number of Entries Returned:  1


$ bin/ldapsearch --hostname ldap.example.com \
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=jdoe,ou=People,dc=example,dc=com" \
     --bindPassword originalPassword \
     --baseDN "dc=example,dc=com" \
     --scope base \
     "(objectClass=*)"
# Bind Result:
#      Result Code:  0 (success)
#      Password Expiring Response Control:
#           OID:  2.16.840.1.113730.3.4.5
#           Seconds Until Expiration:  3317

dn: dc=example,dc=com
objectClass: top
objectClass: domain
dc: example

# Result Code:  0 (success)
# Number of Entries Returned:  1

As you can see, in this second case when we used the original password rather than the new one, the server returned the password expiring response control indicating that the former password was only valid for another 3317 seconds.

If we wanted to perform another self-change, this time using the password modify extended operation to use a self-change for a new password of “thirdPassword”, and we wanted to include the purge password request control, we could accomplish that as follows:

$ bin/ldappasswordmodify --hostname ldap.example.com \
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --authzID "dn:uid=jdoe,ou=People,dc=example,dc=com" \
     --currentPassword "secondPassword" \
     --newPassword "thirdPassword" \
     --purgeCurrentPassword
The LDAP password modify operation was successful

After this, ldapsearch shows that we can successfully authenticate with the new password, but not with either of the previous old passwords because they have been purged:

$ bin/ldapsearch --hostname ldap.example.com
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=jdoe,ou=People,dc=example,dc=com" \
     --bindPassword thirdPassword \
     --baseDN "dc=example,dc=com" \
     --scope base \
     "(objectClass=*)"
dn: dc=example,dc=com
objectClass: top
objectClass: domain
dc: example

# Result Code:  0 (success)
# Number of Entries Returned:  1


$ bin/ldapsearch --hostname ldap.example.com
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=jdoe,ou=People,dc=example,dc=com" \
     --bindPassword secondPassword \
     --baseDN "dc=example,dc=com" \
     --scope base \
     "(objectClass=*)"
# Bind Result:
# Result Code:  49 (invalid credentials)

# An error occurred while attempting to create a connection pool to communicate with the directory server:
# LDAPException(resultCode=49 (invalid credentials), errorMessage='invalid credentials', ldapSDKVersion=4.0.10,
# revision=c8659b0364e0ccaec7a4925f47c184907557a5db)


$ bin/ldapsearch --hostname ldap.example.com
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=jdoe,ou=People,dc=example,dc=com" \
     --bindPassword originalPassword \
     --baseDN "dc=example,dc=com" \
     --scope base \
     "(objectClass=*)"
# Bind Result:
# Result Code:  49 (invalid credentials)

# An error occurred while attempting to create a connection pool to communicate with the directory server:
# LDAPException(resultCode=49 (invalid credentials), errorMessage='invalid credentials', ldapSDKVersion=4.0.10,
# revision=c8659b0364e0ccaec7a4925f47c184907557a5db)

Soft Deletes in the Ping Identity Directory Server

As its name implies, the LDAP delete operation removes an entry from the directory server. Typically, this completely removes the entry from the server, but there may be times when you would prefer for the entry to be hidden from LDAP clients, while still available in the server for at least a period of time.

The Ping Identity Directory Server offers this capability in the form of soft deletes. A soft-deleted entry still exists in the server, but it is renamed so that the DN includes the entry’s entryUUID value, and a special ds-soft-delete-entry object class is added that ensures the entry won’t be visible to most clients, and to provide additional metadata about the soft delete operation (including the entry’s original DN, the time the entry was soft-deleted, and the authorization DN and IP address of the client that requested it).

Using soft deletes can offer a number of benefits. Some of them may include:

  • It makes it easier to resurrect an entry if it is removed in error. We also provide a simple way to undelete a soft-deleted entry to restore it to “regular entry” status.
  • It provides LDAP-accessible auditing information about the delete operation. Even though soft-deleted entries aren’t visible to most clients, we do provide ways for authorized clients to see them if they’re specifically looking for them.
  • You can use this to prevent reuse of values, even after an entry has been deleted. For example, say that you’re an email provider and you don’t ever want to allow an email address to be reused, even if the former owner has removed their account. The unique attribute plugin has support for either permitting or rejecting conflicts with soft-deleted entries.

There are two ways that you can perform soft deletes in the Ping Identity Directory Server: you can configure the server to automatically turn regular deletes matching a given set of criteria into soft deletes, or you can explicitly request them with the soft delete request control. But before you can do either one, you need to set a soft delete policy.

Configuring the Server’s Soft Delete Policy

A soft delete policy can be used to specify the conditions under which the server should automatically turn regular delete operations into soft deletes, and can also be used to indicate the conditions under which the server should automatically clean up soft-deleted entries.

There are two properties that can be used to specify the conditions under which the server should automatically turn regular deletes into soft deletes:

  • auto-soft-delete-connection-criteria — A reference to a connection criteria object that specifies the clients whose delete requests should automatically be turned into soft deletes. This criteria can include anything the server knows about the requester, including their identity (where their entry is in the DIT, the contents of their entry, their group memberships, etc.), the address of the client, whether the communication is secure, and the protocol they are using to communicate with the server.
  • auto-soft-delete-request-criteria — A reference to a request criteria object that specifies which delete requests should automatically be turned into soft deletes. This criteria can include anything the server knows about the delete request, including the location of the target entry in the DIT, the content of that entry, the groups in which that entry is a member, the controls included in the request, and the origin of the request (e.g., directly requested by a client, replicated from another server, initiated by a component within the server, etc.).

If neither of these properties has a value, then only delete requests that include the soft delete request control will be treated as soft deletes. If only one of them has a value, then all delete requests that match that criteria object (or that include the soft delete request control) will be treated as soft deletes. If both of them have values, then only delete requests that match both sets of criteria (or that include the soft delete request control) will be treated as soft deletes.

By default, soft-deleted entries will remain in the server forever (or until someone explicitly deletes them), but you can also configure the server to automatically delete them under certain conditions. The soft delete policy offers two properties that can be used to control this:

  • soft-delete-retention-time — The maximum length of time that soft-deleted entries should be retained in the server before they are eligible to be automatically removed.
  • soft-delete-retain-number-of-entries — The maximum number of soft-deleted entries that should be retained in the server.

If either or both of these properties is configured, then soft-deleted entries that fall outside of either one of them will be eligible for removal. If neither is configured, then soft-deleted entries won’t be automatically removed by the server.

To enable soft delete functionality in the server, you need to create a soft delete policy, and you also need to update the global configuration to make it the active policy. With no active soft delete policy, the server will not automatically turn any deletes into soft deletes, nor will it allow clients to use the soft-delete request control.

Example 1: Only Explicit Soft Deletes Without Automatic Cleanup

If you don’t want the server automatically turning regular deletes into soft deletes, but you do want to allow clients to use the soft delete request control, and if you don’t want the server to automatically clean up any soft-deleted entries, then you can just create a soft delete policy with the default settings and make that the active policy. You can do that with the following configuration changes:

dsconfig create-soft-delete-policy \
     --policy-name "Explicit Soft Delete Requests Without Cleanup"

dsconfig set-global-configuration-prop \
     --set "soft-delete-policy:Explicit Soft Delete Requests Without Cleanup"

Example 2: Automatic Soft Deletes With Automatic Cleanup

If you want the server to automatically turn all delete operations into soft deletes, and to keep soft-deleted entries around for 30 days, you can do that with the following changes:

dsconfig create-request-criteria \
     --criteria-name "All Delete Requests" \
     --type simple \
     --set operation-type:delete

dsconfig create-soft-delete-policy \
     --policy-name "Automatic Soft Deletes" \
     --set "auto-soft-delete-request-criteria:All Delete Requests" \
     --set "soft-delete-retention-time:30 d"

dsconfig set-global-configuration-prop \
     --set "soft-delete-policy:Automatic Soft Deletes"

The Soft and Hard Delete Controls

The Soft Delete Request Control

If you want to explicitly control which delete requests get turned into soft deletes, then you can include the soft delete request control in the delete request. This request control has an OID of “1.3.6.1.4.1.30221.2.5.20”, and it can optionally have a value. If there is a value, then it should have the following ASN.1 encoding:

SoftDeleteRequestValue ::= SEQUENCE {
     returnSoftDeleteResponse     [0] BOOLEAN DEFAULT TRUE,
     ... }

The Soft Delete Response Control

If the request control doesn’t have a value, or if it has a value with the returnSoftDeleteResponse flag set to true, then the delete result may include a soft delete response control with an OID of “1.3.6.1.4.1.30221.2.5.21” and whose value is simply the string representation of the DN for the soft-deleted entry. The soft-deleted entry DN will be the same as the original DN, but with the RDN updated to include the entry’s entryUUID attribute value. For example, if the entry “uid=jdoe,ou=People,dc=example,dc=com” has an entryUUID value of “53e84e32-4be9-4ed6-b489-88d8bea4bdcd”, then the resulting DN for the soft-deleted entry would be “entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com”. The soft-deleted entry will also include the ds-soft-delete-entry object class, and it will include a ds-soft-delete-from-dn attribute whose value was the DN of the original entry and a ds-soft-delete-timestamp attribute whose value reflects the time that the soft delete operation was performed.

The Hard Delete Request Control

We also offer a hard delete request control, which can be used to explicitly indicate that an entry should be completely removed, even if the server would have otherwise automatically turned the delete operation into a soft delete. The hard delete request control has an OID of “1.3.6.1.4.1.30221.2.5.22” and no value. There is no corresponding hard delete response control.

Using the Soft and Hard Delete Controls With ldapmodify

The ldapmodify command-line tool offers support for the soft delete request control via the “--softDelete” argument, and for the hard delete request control via the “--hardDelete” argument.

For example, the following can be used to remove the “uid=jdoe,ou=People,dc=example,dc=com” entry using a soft delete operation:

$ bin/ldapmodify --hostname ldap.example.com \
     --port 636 \
     -useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=admin,dc=example,dc=com" \
     --softDelete
Enter the bind password:

# Successfully connected to ldap.example.com:636.

dn: uid=jdoe,ou=People,dc=example,dc=com
changetype: delete

# Deleting entry uid=jdoe,ou=People,dc=example,dc=com ...
# Result Code:  0 (success)
# Soft Delete Response Control:
#      OID:  1.3.6.1.4.1.30221.2.5.21
#      Soft-Deleted Entry DN:  entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com

Using the Soft and Hard Delete Controls With the UnboundID LDAP SDK for Java

The UnboundID LDAP SDK for Java supports the soft delete controls via the SoftDeleteRequestControl and the SoftDeleteResponseControl classes. It supports the hard delete request control via the HardDeleteRequestControl class. The class-level Javadoc documentation for the SoftDeleteRequestControl includes an example that demonstrates the use of these controls, and the related undelete and soft-deleted entry access request controls.

The Soft-Deleted Entry Access Request Control

There wouldn’t be much benefit to having soft-deleted entries if we didn’t provide a way to get access to them. By default, soft-deleted entries are hidden from clients, so they won’t be included in search results. However, there are two ways that you can access soft-deleted entries:

  • If you know the soft-deleted entry’s DN, then you can retrieve that entry with a search using the baseObject scope.
  • If you issue a search with the soft-deleted entry access request control, then soft-deleted entries can be included in the search results.

The latter option is much more useful than the former because it’s hard to know what a soft-deleted entry’s DN is unless you knew that entry’s UUID value before it was deleted, or you got the soft-deleted entry DN from the soft delete response control.

The soft-deleted entry access request control has an OID of “1.3.6.1.4.1.30221.2.5.24”. It may optionally have a value, and if it does, then that value must have the following ASN.1 encoding:

SoftDeleteAccessRequestValue ::= SEQUENCE {
     includeNonSoftDeletedEntries     [0] BOOLEAN DEFAULT TRUE,
     returnEntriesInUndeletedForm     [1] BOOLEAN DEFAULT FALSE,
     ... }

The includeNonSoftDeletedEntries element of the request control indicates whether the server should include non-soft-deleted entries in the search results. If this is true (which is the default), then the set of entries returned may include both soft-deleted and non-soft-deleted entries. If this is false, then only soft-deleted entries will be returned.

The returnEntriesInUndeletedForm element of the request control indicates whether matching soft-deleted entries should be returned in their undeleted form (if true) rather than their soft-deleted form (if false). The main difference between these forms is that the undeleted form will have the entry’s original DN rather than the soft-deleted DN that includes the entryUUID attribute, and will not include the ds-soft-delete-entry object class or the ds-soft-delete-from-dn or ds-soft-delete-timestamp attributes.

If the request control doesn’t have a value, then it will behave as if you provided a value with includeNonSoftDeletedEntries set to true and returnEntriesInUndeletedForm set to false.

Using the Soft-Deleted Entry Access Request Control With ldapsearch

The ldapsearch command-line tool offers support for the soft-deleted entry access request control through the --includeSoftDeletedEntries argument. This argument must take a value, and that value should be one of the following:

  • with-non-deleted-entries — Indicates that both soft-deleted and non-soft-deleted entries should be included in the search results. Soft-deleted entries will be returned in their soft-deleted form.
  • without-non-deleted-entries — Indicates that only soft-deleted entries should be returned, in their soft-deleted form. Non-soft-deleted entries will not be returned.
  • deleted-entries-in-undeleted-form — Indicates that only soft-deleted entries should be returned, but they should be returned in their undeleted form.

For example, if you wanted to search for the soft-deleted entry with a uid value of jdoe, you could use a command like:

$ bin/ldapsearch --hostname ldap.example.com \
     --port 636 \
     --useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=admin,dc=example,dc=com" \
     --baseDN "dc=example,dc=com" \
     --scope sub \
     --requestedAttribute "*" \
     --requestedAttribute "+" \
     --includeSoftDeletedEntries without-non-deleted-entries \
     "(uid=jdoe)"
Enter the bind password:

# Soft Delete Response Control:
#      OID:  1.3.6.1.4.1.30221.2.5.21
#      Soft-Deleted Entry DN:  entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com
dn: entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com
objectClass: top
objectClass: person
objectClass: organizationalPerson
objectClass: inetOrgPerson
objectClass: ds-soft-delete-entry
sn: Doe
cn: John Doe
givenName: John
uid: jdoe
createTimestamp: 20190227170715.814Z
creatorsName: cn=Directory Manager,cn=Root DNs,cn=config
modifyTimestamp: 20190227170715.814Z
modifiersName: cn=Directory Manager,cn=Root DNs,cn=config
entryUUID: 53e84e32-4be9-4ed6-b489-88d8bea4bdcd
ds-soft-delete-from-dn: uid=jdoe,ou=People,dc=example,dc=com
ds-soft-delete-timestamp: 20190227170734.870Z
ds-entry-checksum: 2440630426
subschemaSubentry: cn=schema

# Result Code:  0 (success)
# Number of Entries Returned:  1

Using the Soft-Deleted Entry Access Request Control With the UnboundID LDAP SDK for Java

The UnboundID LDAP SDK for Java provides support for the soft-deleted entry access request control through the SoftDeletedEntryAccessRequestControl class. The class-level Javadoc documentation for the SoftDeleteRequestControl class provides an example that demonstrates how to use this control (along with the soft delete, hard delete, and undelete request controls).

The Undelete Request Control

Support for soft deletes would also not be very useful if we didn’t provide a way to restore a soft-deleted entry back to being a regular, non-soft-deleted entry. And we do offer that ability through the undelete request control. If you include this control in a specially crafted add request, then the server will restore the target entry back to its former glory. The undelete request control has an OID of “1.3.6.1.4.1.30221.2.5.23”, and it does not need a value. There is no corresponding response control.

Note that I mentioned a “specially crafted add request” in that last paragraph. The server handles the undelete operation as an add operation, but if the undelete request control is present, then the contents of that add request will be a little different from when you’re adding an entry from scratch. Here’s what you need to include:

  • The DN included in the add request should be the DN that you want the undeleted to have. If you want this to be the entry’s original DN, then you could use the value of the soft-deleted entry’s ds-soft-delete-from-dn attribute, but you can choose something else if you want the restored entry to have a different DN.
  • The add request must include a ds-undelete-from-dn attribute whose value is the DN of the soft-deleted entry that you want to undelete.

Using the Undelete Request Control With ldapmodify

The ldapmodify tool supports the use of the undelete request control through the “--allowUndelete” argument. If you add this argument, then the undelete request control will automatically be included in any add requests that it sends. For example:

$ bin/ldapmodify --hostname ldap.example.com \
     --port 636 \
     -useSSL \
     --trustStorePath config/truststore \
     --bindDN "uid=admin,dc=example,dc=com" \
     --allowUndelete
Enter the bind password:

# Successfully connected to ldap.example.com:636.

dn: uid=jdoe,ou=People,dc=example,dc=com
changetype: add
ds-undelete-from-dn: entryUUID=53e84e32-4be9-4ed6-b489-88d8bea4bdcd+uid=jdoe,ou=People,dc=example,dc=com

# Adding entry uid=jdoe,ou=People,dc=example,dc=com ...
# Result Code:  0 (success)

Using the Undelete Request Control with the UnboundID LDAP SDK for Java

The UnboundID LDAP SDK for Java supports the undelete request control via the UndeleteRequestControl class. This class even provides a helpful createUndeleteRequest convenience method that allows you to construct an appropriate add request when provided with the DN that you want the undeleted entry to have and the DN of the soft-deleted entry that you want to undelete. As noted above, the class-level Javadoc documentation for the SoftDeleteRequestControl class provides an example that demonstrates how to use all of the controls related to soft-delete processing.

The Multi-Update Extended Operation in the Ping Identity Directory Server

In an earlier post, I mentioned that while you can process multiple searches in parallel to speed up an application that needs multiple pieces of information to do its work, that generally only works if those searches are independent. If the searches are related (meaning, that you need the results of one to construct another request), then this approach won’t work. Fortunately, the Ping Identity Directory Server offers an LDAP join control that allows you to perform a search that not only retrieves the entries matching the search criteria, but that also joins those entries with related entries as specified by the join rule.

Wouldn’t it be nice if there were something similar for write operations? While LDAP allows you to send multiple write requests concurrently on the same or different connections, you can’t really do that if there are dependencies between those write operations. For example, let’s say that you want to add an entry along with some subordinate entries. You can’t add a child before creating its parent, and if you try to send them in parallel, then maybe it’ll work and maybe it won’t. But here again, the Ping Identity Directory Server has you covered, this time in the form of the multi-update extended operation.

As its name implies, the multi-update operation allows you to send multiple updates (any combination of add, delete, modify, modify DN, and password modify extended operations) in a single request that will be processed in the order that you provide them. At the very least, this allows you to reduce the amount of time required to process those operations because there’s only one round trip between the client and the server. But it also allows you to decide what happens if an error occurs while processing any of those updates, and here you have three options:

  • You can have the processing occur atomically so that no changes will be applied unless all of them are processed successfully, and so that no client will be able to see the data in an intermediate state with only some of the changes completed. You can get this same benefit from LDAP transactions as described in RFC 5805 (which the Ping Identity Directory Server also supports), but the multi-update operation is more efficient because there’s only a single request and response, whereas LDAP transactions require a separate network round trip for each of the changes, plus additional round trips when starting and ending the transaction.
  • You can have processing stop after the first error. Any writes that succeeded before the error will be preserved, but any changes in the multi-update request after the one that caused the error will be ignored. Clients may be able to see the data in an intermediate state while these operations are being processed.
  • You can have processing continue until all of the operations have been attempted. Any of the changes that are successful will remain in place, and again, clients may be able to see the data in an intermediate state while they are being processed.

The Multi-Update Extended Request

The multi-update extended request has an OID of 1.3.6.1.4.1.30221.2.6.17 and a value with the following ASN.1 encoding:

MultiUpdateRequestValue ::= SEQUENCE {
     errorBehavior     ENUMERATED {
          atomic              (0),
          quitOnError         (1),
          continueOnError     (2),
          ... },
     requests          SEQUENCE OF SEQUENCE {
          updateOp     CHOICE {
               modifyRequest     ModifyRequest,
               addRequest        AddRequest,
               delRequest        DelRequest,
               modDNRequest      ModifyDNRequest,
               extendedReq       ExtendedRequest,
               ... },
          controls     [0] Controls OPTIONAL,
          ... },
     ... }

As you might expect, the request just specifies the behavior to use in case an error is encountered during processing and the set of requests to be processed. Note that while the ASN.1 definition above does allow for any kind of extended request to be included, the only one that the Ping Identity Directory Server currently allows in a multi-update request in the password modify extended request.

The UnboundID LDAP SDK for Java offers support for the multi-update extended request through the MultiUpdateExtendedRequest class, with an assist from the MultiUpdateErrorBehavior enum. If you want to use the multi-update extended operation through some other API, you’ll need to encode the request for yourself.

The Multi-Update Extended Result

The multi-update extended result has an OID of 1.3.6.1.4.1.30221.2.6.18 and a value with the following encoding:

MultiUpdateResultValue ::= SEQUENCE {
     changesApplied     ENUMERATED {
          none        (0),
          all         (1),
          partial     (2),
     ... },
     responses     SEQUENCE OF SEQUENCE {
          responseOp     CHOICE {
               modifyResponse     ModifyResponse,
               addResponse        AddResponse,
               delResponse        DelResponse,
               modDNResponse      ModifyDNResponse,
               extendedResp       ExtendedResponse,
               ... },
          controls       [0] Controls OPTIONAL,
          ... },
     ... }

There are two components to the extended result value:

  • An indicator as to whether none, all, or some of the changes were applied.
  • The results for all of the operations that were attempted. The results will be listed in the same order as in the request, and each operation result may optionally include the response controls for that operation.

If only a portion of the operations were attempted (for example, because the server stopped processing the multi-update operation after an error was encountered while processing one of the changes and did not attempt any of the others after that), then there may be fewer results than there were requests.

The UnboundID LDAP SDK for Java offers support for the multi-update extended result through the MultiUpdateExtendedResult class and the MultiUpdateChangesApplied enum. To use this extended operation in another API, you’ll need to decode the result value on your own.

Supported Controls

There are two categories of controls that may be used in conjunction with the multi-update extended request: those that can be attached to the multi-update extended operation itself, and those that can be attached to the individual operation requests inside the multi-update request value.

The controls that may be attached to the multi-update extended operation itself are:

  • Get Backend Set ID (only for atomic requests)
  • Intermediate Client
  • Proxied Authorization v1
  • Proxied Authorization v2
  • Route To Backend Server (only for atomic requests)
  • Transaction Settings (only for atomic requests)

The controls that may be attached to operation requests inside the multi-update request value are:

  • Account Usable
  • Assertion
  • Intermediate Client
  • Get Backend Set ID (only for non-atomic requests)
  • Hard Delete
  • Manage DSA IT
  • Password Policy
  • Post-Read
  • Pre-Read
  • Replication Repair
  • Route To Backend Server (only for non-atomic requests)
  • Soft Delete
  • Subtree Delete
  • Undelete

An Example Using the UnboundID LDAP SDK for Java

The ldapmodify tool provided as part of the UnboundID LDAP SDK for Java already includes support for the multi-update extended operation (via the --multiUpdateErrorBehavior argument), and you can find the code for that tool at https://github.com/pingidentity/ldapsdk/blob/master/src/com/unboundid/ldap/sdk/unboundidds/tools/LDAPModify.java.

However, that version of ldapmodify has a lot of features, and the multi-update support is only a tiny portion of it. For the sake of clearer illustration, I wrote a much simpler command-line tool that serves as a clearer demonstration of the multi-update operation. It operates much like ldapmodify, but it only reads the changes from an LDIF file, and it sends them to the server all at once through a multi-update operation. You can find that example at https://github.com/dirmgr/blog-example-source-code/tree/master/multi-update.

The LDAP Join Control in the Ping Identity Directory Server

LDAP is an asynchronous protocol, and most directory servers are very good when it comes to handling multiple requests simultaneously, whether on the same connection or multiple connections. If an application needs to issue multiple independent searches in the course of performing some function, and if it wants to get the results in as little time as possible, then it can issue those searches concurrently rather than sequentially. This means that the application should have all the results it needs in the time required to process the longest operation, as opposed to the sum of the times required to process those operations (not to mention the network round-trip time, which gets magnified when issuing requests in series rather than in parallel).

However, you can’t use this approach if there are dependencies between the searches that you want to perform. For example, let’s say that when an employee logs in, you want to show them a portion of their organizational chart that includes their manager and their peers (that is, the other employees who share the same manager). Normally, you’d need to perform three searches:

  • One search to retrieve the entry for the user who is authenticating and get the manager attribute
  • One to retrieve the entry for the user referenced by the employee’s manager attribute
  • One to retrieve the entries of all users with that same manager value

While you could potentially issue the second and third searches concurrently, you have to wait for the results of the first search to have the information you need for the other two.

In the Ping Identity Directory Server, we provide support for a feature that can allow you to do all of this with a single request: the LDAP join control. As its name implies, it offers an LDAP take on the SQL join you can perform in a relational database. If you issue a search request that includes the join request control, then each entry that matches the search criteria will be joined with “related” entries (in accordance with the criteria in the join request control).

The Join Request Control

The join request control describes the relationship that you want to use to identify other entries that are in some way related to the entries matching your search request. The request control has an OID of 1.3.6.1.4.1.30221.2.5.9 and a value with the following ASN.1 encoding:

LDAPJoin ::= SEQUENCE {
     joinRule         JoinRule,
     baseObject       CHOICE {
          useSearchBaseDN      [0] NULL,
          useSourceEntryDN     [1] NULL,
          useCustomBaseDN      [2] LDAPDN,
          ... },
     scope            [0] ENUMERATED {
          baseObject             (0),
          singleLevel            (1),
          wholeSubtree           (2),
          subordinateSubtree     (3),
          ... } OPTIONAL,
     derefAliases     [1] ENUMERATED {
          neverDerefAliases       (0),
          derefInSearching        (1),
          derefFindingBaseObj     (2),
          derefAlways             (3),
          ... } OPTIONAL,
     sizeLimit        [2] INTEGER (0 .. maxInt) OPTIONAL,
     filter           [3] Filter OPTIONAL,
     attributes       [4] AttributeSelection OPTIONAL,
     requireMatch     [5] BOOLEAN DEFAULT FALSE,
     nestedJoin       [6] LDAPJoin OPTIONAL,
     ... }

JoinRule ::= CHOICE {
     and               [0] SET (1 .. MAX) of JoinRule,
     or                [1] SET (1 .. MAX) of JoinRule,
     dnJoin            [2] AttributeDescription,
     equalityJoin      [3] JoinRuleAssertion,
     containsJoin      [4] JoinRuleAssertion,
     reverseDNJoin     [5] AttributeDescription,
     ... }

JoinRuleAssertion ::= SEQUENCE {
     sourceAttribute     AttributeDescription,
     targetAttribute     AttributeDescription,
     matchAll            BOOLEAN DEFAULT FALSE }

Most of the fields of the join request should be familiar because they’re similar to the fields of an ordinary search request. But I’ll go ahead and call them all out anyway. Also note that the Javadoc for the JoinRequestControl, JoinRequestValue, JoinRule, and JoinBaseDN classes in the UnboundID LDAP SDK for Java may provide additional information.

The Join Rule

The first, and probably most important, field of a join request control is the join rule. This is used to specify the types of entries that should be joined with the corresponding search result entry. We currently offer six types of join rules (and may add support for more in the future):

  • AND — An AND join rule encapsulates a set of one or more other join rules and will only join a search result entry with entries that match the criteria for all of the encapsulated join rules.
  • OR — An OR join rule encapsulates a set of one or more other join rules and will only join a search result entry with entries that match the criteria for at least one of the encapsulated join rules.
  • DN Join — A DN join rule will join a search result entry with other entries whose DNs are contained in a specified attribute in the search result entry. For example, You could use a DN join rule to join a groupOfNames entry with the entries whose DNs are contained in the member attribute of the group. Or you could join an employee’s entry with the entry of their boss via the manager attribute in the employee’s entry.
  • Equality Join — An equality join rule will join a search result entry with other entries that share a common attribute value. For example, say that a mobile phone service provider has an entry for each account, and an entry for each device linked to an account. If the account entry has an accountNumber attribute, and the devices associated with that account also contain that same accountNumber value, you could use an equality join to associate the device entries with the account. Also note that the value that the joined entries have in common doesn’t necessarily have to be in the same attribute type (for example, it could be in the accountNumber attribute of an account entry and the deviceAccountNumber attribute of a device entry).
  • Contains Join — A contains join rule is much like an equality join rule, except that the server uses a substring match (and more correctly, a subAny match) instead of an equality match when identifying the entries to join with the search result entry. That is, the value of a specified attribute in the search result entry must be equal to or a substring of a value in a specified attribute in the joined entries.
  • Reverse DN Join — A reverse DN join rule will join a search result entry with entries that contain the DN of that search result entry in the value of a specified attribute. For example, you could use a reverse DN join to retrieve the entries for a user’s direct reports via the manager attribute.

The Join Base DN

The base DN field of a join request is expressed a little bit differently than the base DN from a search request. In a search request, you must always specify the topmost entry in the subtree containing the entries you’re interested in retrieving. The base DN field of a join request has the same purpose, but there are three different ways that you can indicate what that base DN should be:

  • You can indicate that the base DN from the search request should also be the base DN used when finding entries to be joined with each search result entry.
  • You can indicate that the DN of each search result entry should be used as the base DN used when finding entries to be joined with that search result entry.
  • You can explicitly specify the base DN that you want to use for the join request.

The Join Scope

The scope field of a join request has basically the same meaning as the scope field of a search request, except that it’s relative to the join base DN rather than the search base DN. Those scopes are:

  • baseObject — This indicates that only the entry specified by the join base DN may be joined with the search result entry. None of its subordinates will be included.
  • singleLevel — This indicates that only the entries that are the immediate subordinates of the entry specified by the join base DN may be joined with the search result entry. The join base entry itself will not be included, nor will entries more than one level below the join base entry.
  • wholeSubtree — This indicates that the join base entry and all of its subordinates, to any depth, may be joined with the search result entry.
  • subordinateSubtree — This indicates that all entries below the join base entry, to any depth, may be joined with the search result entry. The join base entry itself will not be included.

Note that the scope element of a join request control is optional, and if it is not provided, then the scope from the search request will be used.

The Join Alias Dereferencing Policy

The alias dereferencing policy in the join request control has the same meaning as in the search request itself. It tells the server how it should treat any aliases that are encountered during join processing. The allowed values include:

  • neverDerefAliases — Indicates that the server should not attempt to dereference any aliases encountered during join processing.
  • derefInSearching — Indicates that the server should attempt to dereference any aliases encountered below the join base DN, but not the join base DN itself if it happens to be an alias.
  • derefFindingBaseObj — Indicates that the server should attempt to dereference the join base DN if it happens to be an alias, but not any aliases encountered below that entry.
  • derefAlways — Indicates that the server should attempt to dereference any aliases it encounters during join processing.

As with the join scope, this is an optional element in a join request control. If you leave it out, the server will use the same policy as specified in the search request.

The Join Size Limit

This specifies the maximum number of entries that should be joined with each search result entry. If a search result entry would have been joined with more than 1000 entries, then the join result control associated with that entry will include a “size limit exceeded” join result code and will not include any of the joined entries.

Note that the server may impose a size limit that is lower than the one requested by the client. In particular, the effective size limit, cannot be larger than the requester’s maximum search size limit, the maximum size limit imposed by the requester’s client connection policy, and the maximum join request size limit defined in the global configuration.

This is an optional element, and if it is not specified, then the size limit from the search request will be used as the size limit for the join processing.

On a related note, the join request control does not include an element to specify a time limit. The time limit from the search request applies to the entire set of processing for the request.

The Join Filter

This is an optional additional search filter that will be required to match any entry for it to be joined with a search result entry. By default, all entries within the scope of the join request that match the criteria specified by the join rule will be joined with the search result entry. If an additional filter is specified, then entries will also have to match that filter to be included in the join.

The Join Attributes

This specifies the set of attributes that should be included in the entries that are joined with a search result entry. This works in the same way as the set of requested attributes for the search request itself, and it can include special tokens like “*” (to indicate that all user attributes should be included), “+” (to indicate that all operational attributes should be included), and “@” followed by an object class name (to indicate that all attributes associated with that object class should be included). If this element is missing or empty, then the default behavior will be to return all user attributes.

Note that the set of attributes that will actually be included in joined entries may be less than the set of requested attributes. For example, some attributes may be excluded because the requester does not have access control permission to retrieve them, or because returning them would violate a sensitive attribute constraint.

The Require Match Flag

This indicates whether a search result entry must be joined with at least one other entry for it to be included in the search results. If this is set to true and there are no entries that match the join criteria for a given search result entry, then that search result entry will not be returned to the client. If this is false, or if it is omitted from the join request control, then the search result entry will still be returned.

The Nested Join Element

The nested join element allows you to extend the join processing to more levels. Not only can you join each search result entry with a related set of entries, but you can also join each of those joined entries with even more related entries based on another set of join criteria. Note that if there is a nested join element, then each joined entry effectively becomes the search result entry for the next level of the join. And if you want to have more than two levels of joins, nested joins can include their own nested joins, but you probably don’t want to go too deep because it has the potential to make the join result really big.

For example, in the scenario we described above where you want to join an employee to their manager and peers, the search request could be used to find the employee’s entry, and you could use a DN join to join it with their manager’s entry (based on the manager attribute in the employee’s entry), and then you could use a reverse DN join to join the manager with their direct reports (also by the manager attribute in their entries).

The Join Result Control

If the search request includes a join request control, then each search result entry will include a join result control that provides information about the join processing for that entry. The join result control has an OID of 1.3.6.1.4.1.30221.2.5.9 and a value with the following encoding:

JoinResult ::= SEQUENCE {
     COMPONENTS OF LDAPResult,
     entries     [4] SEQUENCE OF JoinedEntry }

JoinedEntry ::= SEQUENCE {
     objectName            LDAPDN,
     attributes            PartialAttributeList,
     nestedJoinResults     SEQUENCE OF JoinedEntry OPTIONAL }

So basically, the join result control contains a result code, an optional diagnostic message, an optional matched DN, an optional set of referral URLs, and a list of the entries that were joined with the search result entry. And if the join request included a nested join, then each joined entry can have its own set of joined entries.

See the JoinResultControl and JoinedEntry classes in the UnboundID LDAP SDK for Java for more information.

An Example Using the ldapsearch Tool

The ldapsearch command-line tool shipped with the Ping Identity Directory Server (and also with the UnboundID LDAP SDK for Java) includes support for the LDAP join control through the following arguments:

  • --joinRule — The join rule to use. This is the only argument that is required to include the join request control in the search request. The value must be in one of the following formats:

    • dn:{sourceAttribute} — Indicates that each search result entry should be joined with entries whose DNs are contained in the specified source attribute of the search result entry.
    • reverse-dn:{targetAttribute} — Indicates that each search result entry should be joined with entries that contain the DN of the search result entry in the specified target attribute.
    • equals:{sourceAttribute}:{targetAttribute} — Indicates that each search result entry should be joined with entries that have the value of the search result entry’s source attribute in the joined entry’s target attribute.
    • contains:{sourceAttribute}:{targetAttribute} — Indicates that each search result entry should be joined with entries that contain the value of the search result entry’s source attribute as a substring in the joined entry’s target attribute.
  • --joinBaseDN — The join base DN to use. If this is omitted, then the base DN from the search request will be used. If it is provided, the value can be one of the following:

    • The string “search-base”, which indicates that the base DN of the search request should also be used as the join base DN.
    • The string “source-entry-dn”, which indicates that the DN of the search result entry should be used as the join base DN.
    • Any valid LDAP DN, which will be used as the join base DN.
  • --joinScope — The scope for the join processing. If this is omitted, then the scope from the search request will be used. If it is provided, then the value may be one of the following:

    • base — The baseObject scope.
    • one — The singleLevel scope.
    • sub — The wholeSubtree scope.
    • subordinates — The subordinateSubtree scope.
  • --joinSizeLimit — The maximum number of entries that should be joined with each search result entry. If this is omitted, then the search request size limit will be used.
  • --joinFilter — An additional filter that joined entries will be required to match. If this is omitted, then no additional filter will be used.
  • --joinRequestedAttribute — The name or OID of an attribute that should be included in joined entries. This can be specified multiple times to indicate that multiple attributes should be included. If this is omitted, then all user attributes will be requested.
  • --joinRequireMatch — If present, this indicates that search result entries that aren’t joined with any entries should be omitted from the results.

As you might have noticed, the ldapsearch tool doesn’t quite provide full support for all of the LDAP join features. For example, it doesn’t offer the AND or OR join rule types, and it doesn’t allow you to perform a nested join. But it’s still good enough to let you try out the join control in a number of common cases.

The following is an example that demonstrates using ldapsearch to perform a DN join that links an employee’s entry to the entry of their boss via the manager attribute:

$ bin/ldapsearch --hostname ds.example.com \
     --port 636 \
     --useSSL \
     --bindDN 'cn=LDAP Join Example,ou=Applications,dc=example,dc=com' \
     --joinRule dn:manager \
     --joinBaseDN search-base \
     --joinScope sub \
     --joinRequestedAttribute givenName \
     --joinRequestedAttribute sn \
     --joinRequestedAttribute mail \
     --joinRequestedAttribute telephoneNumber \
     --baseDN dc=example,dc=com \
     --scope sub "(uid=ernest.employee)" \
     givenName \
     sn \
     mail \
     telephoneNumber
Enter the bind password:

The server presented the following certificate chain:

     Subject: CN=ds.example.com,O=Ping Identity Self-Signed Certificate
     Valid From: Friday, February 8, 2019 at 12:23:34 AM CST
     Valid Until: Friday, February 4, 2039 at 12:23:34 AM CST
     SHA-1 Fingerprint: 78:f1:49:a0:06:0b:bd:1e:2c:88:cb:76:60:cb:87:cb:c4:c3:76:97
     256-bit SHA-2 Fingerprint: 55:9c:9a:54:97:48:8c:51:fa:10:da:a0:08:f0:15:dc:f0:92:75:3e:e9:be:56:c5:5c:5c:ec:d5:d4:85:15:a2

WARNING:  The certificate is self-signed.

Do you wish to trust this certificate?  Enter 'y' or 'n': y
# Join Result Control:
#      OID:  1.3.6.1.4.1.30221.2.5.9
#      Join Result Code:  0 (success)
#      Joined With Entry:
#           dn: uid=betty.boss,ou=People,dc=example,dc=com
#           mail: betty.boss@example.com
#           sn: Boss
#           givenName: Betty
#           telephoneNumber: +1 123 456 7891
dn: uid=ernest.employee,ou=People,dc=example,dc=com
mail: ernest.employee@example.com
sn: Employee
givenName: Ernest
telephoneNumber: +1 123 456 7890

# Result Code:  0 (success)
# Number of Entries Returned:  1

An Example Using the UnboundID LDAP SDK for Java

I’ve also written an example that demonstrates the use of the LDAP join control in the UnboundID LDAP SDK for Java. The LDAP SDK does support using nested joins, so this example uses the scenario outlined above, in which we retrieve a user, their manager, and their peers. You can find it at https://github.com/dirmgr/blog-example-source-code/tree/master/ldap-join.

The Get Password Policy State Issues Control in the Ping Identity Directory Server

In the Ping Identity Directory Server, we’re very serious when it comes to security. We make it easy to encrypt all your data, including the database contents (and the in-memory database cache), network communication, backups, LDIF exports, and even log files. We’ve got lots of password policy features, like strong password encoding, many password validation options, and ways to help thwart password guessing attempts. We offer several two-factor authentication options. We have a powerful access control subsystem that is augmented with additional features like sensitive attributes and privileges. We have lots of monitoring and alerting features so that you can be notified of any problems as soon as (or, in many cases, before) they arise so that your service remains available. Security was a key focus back when I started writing OpenDS (which is the ancestor of the Ping Identity Directory Server), and it’s still a key focus today.

One small aspect of this focus on security is that, by default, we don’t divulge any information about the reason for a failed authentication attempt. Maybe the account doesn’t exist, or maybe it’s locked or administratively disabled. Maybe the password was wrong, or maybe it’s expired. Maybe the user isn’t allowed to authenticate from that client system. In all of these cases, and for other types of authentication failures, the server will just return a bind result with a result code of invalidCredentials and no diagnostic message. The server will include the exact reason for the authentication failure in the audit log so that it’s available for administrators, but we won’t return it to the client so that a malicious user can’t use that to better craft their attack.

Now, if you don’t care about this and want the server to just go ahead and provide the message to the client, then you can do that with the following configuration change:

dsconfig set-global-configuration-prop --set return-bind-error-messages:true

However, that may not be the best option because it applies equally to all authentication requests for all clients, and because the output is human-readable but not very machine parseable. It’s not easy for a client to programmatically determine what the reason for the failure is. For that, your best option is the get password policy state issues control.

The get password policy state issues control indicates that you want the server to return information about the nature of the authentication failure, and details of the user’s password policy state that might interfere with authentication either now or in the future. This information is easy to consume programmatically, but it also contains user-friendly representations of those conditions as well. We intend for this control to be used by applications that authenticate users, and that can decide what information they want to make available to the end user.

Restrictions Around the Control’s Use

As previously mentioned, we might not always want to divulge the reason for a failed authentication attempt to the end user. As such, if we allowed just anyone to use this control, then that would get thrown out the window since a malicious client could just always include that control and get some helpful information in the response. So we don’t do that. Instead, this control will only be permitted if all of the following conditions are met:

  • The server’s access control handler must allow the get password policy state issues request control to be included in bind requests. This control is allowed in bind request by default, but you can disable it if you want to.
  • A bind request that includes the get password policy state issues request control must be received on a connection that is already authenticated as a user who has the permit-get-password-policy-state-issues privilege.

Since we intend this feature to be used by applications that authenticate users, we expect that any application that is to be authorized to use it will have an account with the necessary privilege. And since the get password policy state issues control is a proprietary feature, we expect that any application that knows how to use it can also easily include the retain identity request control in those same bind requests.

The Get Password Policy State Issues Request Control

The get password policy state issues request control is very simple: it’s got a request OID of 1.3.6.1.4.1.30221.2.5.46 and no value. This control is only intended to be included in bind requests, and it’s really just asking the server to include the corresponding response control in the bind result message.

It’s easy enough to use this request control any LDAP API, but if you’re using the UnboundID LDAP SDK for Java, then we provide the GetPasswordPolicyStateIssuesRequestControl class to make it even easier.

The Get Password Policy State Issues Response Control

The get password policy state issues response control is more complicated than the request control. It has an OID of 1.3.6.1.4.1.30221.2.5.47 and a value with the following ASN.1 encoding:

GetPasswordPolicyStateIssuesResponse ::= SEQUENCE {
     notices               [0] SEQUENCE OF SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     warnings              [1] SEQUENCE OF SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     errors                [2] SEQUENCE OF SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     authFailureReason     [3] SEQUENCE {
          type        INTEGER,
          name        OCTET STRING,
          message     OCTET STRING OPTIONAL } OPTIONAL,
     ... }

If you’re using the UnboundID LDAP SDK for Java, then you can use the GetPasswordPolicyStateIssuesResponseControl class to do all the heavy lifting for you. If you’re using some other API, then you’ll probably have to decode the value for yourself.

There are four basic components to the get password policy state issues response control:

  • A set of error conditions in the user’s password policy state that will either prevent that user from authenticating, or that will prevent them from using their account until they take some action. In the UnboundID LDAP SDK for Java, this we offer the PasswordPolicyStateAccountUsabilityError class to make it easier to interpret these errors. Possible password policy state error conditions include:

    • The account is administratively disabled.
    • The account has expired.
    • The account is not yet active.
    • The account is permanently locked (or at least until an administrator unlocks it) after too many failed authentication attempts.
    • The account is temporarily locked after too many failed authentication attempts.
    • The account is locked because it’s been idle for too long.
    • The account is locked because the password was administratively reset, but the user didn’t choose a new password quickly enough.
    • The password is expired.
    • The password is expired, but there are one or more grace logins remaining. Authenticating with a grace login will only permit them to bind for the purpose of changing the password.
    • The password has been administratively reset and must be changed before the user will be allowed to do anything else.
    • The password policy was configured so that all users governed by that policy must change their passwords by a specified time, but the user attempting to authenticate failed to do so.
  • A set of warning conditions in the user’s password policy state that won’t immediately impact their ability to use their account, but that may impact their ability to use the account in the near future unless they take some action. In the UnboundID LDAP SDK for Java, we offer the PasswordPolicyStateAccountUsabilityWarning class to make it easier to interpret these warnings. Possible password policy state warning conditions include:

    • The account will expire in the near future.
    • The password will expire in the near future.
    • The account has been idle for too long and will be locked unless they successfully authenticate in the near future.
    • The account has outstanding authentication failures and may be locked if there are too many more failed attempts.
    • The password policy was configured so that all users governed by that policy must change their password by a specified time, but the user attempting to authenticate has not yet done so.
  • A set of notice conditions that additional information about the user’s password policy state that may be helpful for applications or the end user to know. The UnboundID LDAP SDK for Java provides the PasswordPolicyStateAccountUsabilityNotice class to make it easier to interpret these notices. Possible password policy state notices include:

    • A minimum password age has been configured in the password policy governing the user, and it has been less than that length of time since the user last changed their password. The user will not be permitted to change their password again until the minimum age period has elapsed.
    • The account does not have a static password, so it will not be allowed to authenticate using any password-based authentication mechanism.
    • The account has an outstanding delivered one-time password that has not yet been consumed and is not yet expired.
    • The account has an outstanding password reset token that has not yet been consumed and is not yet expired.
    • The account has an outstanding retired password that has not yet expired and may still be used to authenticate.
  • An authentication failure reason, which provides information about the reason that the bind attempt failed. The UnboundID LDAP SDK for Java offers the AuthenticationFailureReason class to help make it easier to use this information. Possible authentication failure reasons include:

    • The server could not find the account for the user that is trying to authenticate (e.g., the user doesn’t exist, or the authentication ID does not uniquely identify the user).
    • The password or other provided credentials were not correct.
    • There was something wrong with the SASL credentials provided by the client (e.g., they were malformed or out of sequence).
    • The account isn’t configured to support the requested authentication type (e.g., they attempted a password-based bind, but the user doesn’t have a password).
    • The account is in an unusable state. The password policy error conditions should encapsulate the reasons that the account is not usable.
    • The server is configured to require the client to authenticate securely, but the authentication attempt was not secure.
    • The account is not permitted to authenticate in the requested manner (e.g., from the client address or using the attempted authentication type).
    • The bind request was rejected by the server’s access control handle.
    • The authentication attempt failed because a problem was encountered while processing one of the controls included in the bind request.
    • The server is currently in lockdown mode and will only permit a limited set of users to authenticate.
    • The server could not assign a client connection policy to the account.
    • The authentication attempt used a SASL mechanism that was implemented in a third-party extension, and that extension encountered an error while processing the bind request.
    • The server encountered an internal error while processing the bind request.

Each password policy state error, warning, and notice, as well as the authentication failure reason, is identified by a name and a numeric type, and also includes a human-readable message suitable for displaying to the user if you decide that it is appropriate.

The Password Policy State Extended Operation

Although it’s not the focus of this blog post (maybe I’ll write another one about it in the future), I should also point out that you can also use the password policy state extended operation to obtain the list of usability errors, warnings, and notices for a user, along with a heck of a lot more information about the state of the account. You can also use it to alter the state if desired. Since it’s an extended operation, you can’t use it in the course of attempting a bind to get the authentication failure reason. However, you could use it in conjunction with the get password policy state issues control if you feel like you need additional state information about the user’s account state after parsing the information in the get password policy state issues response control.

An Example Using the UnboundID LDAP SDK for Java

I’ve written a simple program that demonstrates the use of the get password policy state issues control to obtain the authentication failure reason and password policy state issues for a specified user. You can find that example at https://github.com/dirmgr/blog-example-source-code/tree/master/password-policy-state-issues.