Announcement

LDAP and Active Directory for Kipchak

Mamluk · · 5 min read
LDAP and Active Directory for Kipchak

Most organisations keep their staff accounts in a directory, and in most of them that directory is Active Directory. An internal API that serves those staff needs to check their passwords against it, and to know which groups they belong to. The LDAP driver connects a Kipchak API to Active Directory or OpenLDAP for both purposes, and for searching and changing the directory itself.

The LDAP driver is part of Kipchak Enterprise. It was listed on the public roadmap, and was tested against OpenLDAP 2.6 and against an Active Directory domain provided by a Samba 4 domain controller.

Logging users in

authenticate() checks a username and password. The driver finds the user with its service account, then binds to the directory as that user on a separate connection, so the service account’s connection never changes identity:

$result = LDAP::get()->authenticate($username, $password);

if (!$result->succeeded()) {
    $this->logger->info('Login refused', ['reason' => $result->failure->value]);

    return Http\Response::json($response, ['error' => 'Invalid credentials'], 401);
}

$user = $result->user;

A refused login carries its reason. On Active Directory the driver distinguishes a wrong password from an account that is disabled, locked or expired, a password that has expired or must be changed, and a login outside permitted hours or from a workstation that is not allowed. The reason is intended for the log. The client should be told only that the credentials were invalid, because naming the reason reveals which usernames exist.

An outage is never reported as a wrong password. If the directory cannot be reached, or it refuses the service account, the driver raises a connection exception, and the API can answer 503 rather than 401.

Two protections are applied before anything is sent. An empty password is refused by the driver, because a directory accepts a bind with an empty password as an anonymous bind and reports success. Every username is escaped before it is placed in a search filter, so a username of * matches nobody rather than everyone.

Groups

An API usually needs to know more than whether a password is correct. It needs to know whether the user is a member of the group that may approve payments, or read payroll data. The driver returns a user’s groups, including groups within groups:

$directory->groupNames('alice');                       // ['developers', 'engineering']
$directory->isMemberOf($result->user, 'engineering');  // by name or by distinguished name
$directory->members('CN=Engineering,OU=Groups,DC=corp,DC=example,DC=com');

On Active Directory, nested membership is resolved by the server in a single search. On OpenLDAP, which has no equivalent, the driver follows the groups level by level and detects cycles.

Searching and changing the directory

Searches are built with a filter class that escapes every value, so a value taken from a request cannot alter the filter. Large result sets are read in pages. If the server’s own size limit cuts a search short, the driver raises an exception rather than returning an incomplete list.

The driver also adds, modifies, renames and deletes entries, and changes passwords. A user can change their own password by proving the current one, including a user whose account requires a new password before they can log in. An administrator can set a password directly. On Active Directory both use the directory’s own password attribute and require an encrypted connection; on OpenLDAP they use the LDAP password modify operation. Helpers convert Active Directory’s binary identifiers and timestamps into readable values.

Built for production directories

  • Encryption is the default. Connections use LDAPS or StartTLS, with certificate verification, private certificate authorities and client certificates. An unencrypted connection, which would send passwords in the clear, must be requested explicitly.
  • Several servers can be listed. They are tried in order, so the API continues to authenticate users while one domain controller is unavailable.
  • Timeouts are short. Connecting is limited to 5 seconds and each operation to 10 seconds by default, so an unresponsive directory does not hold requests.
  • Lookups can be cached. User and group lookups can be kept in worker memory, or shared through Kipchak’s Filecache or Valkey drivers. Passwords and login results are never cached.

FrankenPHP worker mode

Under FrankenPHP’s worker mode, each worker keeps its directory connections between requests. Active Directory closes connections that have been idle for 15 minutes, so the driver replaces a connection that has been idle for longer than a configured period, five minutes by default, before using it. An operation that fails because the directory was restarted is repeated once on a new connection.

Testing in worker mode found that the TLS library used by PHP’s LDAP extension is not safe when several threads open encrypted connections at the same moment, and that concurrent first connections could stop a worker entirely. The driver therefore opens encrypted connections one at a time within each process. Connections are opened rarely, so the effect on requests is small.

Installation

The LDAP driver is part of Kipchak Enterprise and is installed from the Enterprise Composer repository:

composer require kipchak/driver-ldap

It requires PHP’s ldap extension, which the Kipchak Enterprise runtime image includes. The configuration reference, including the defaults for Active Directory and OpenLDAP, is in the LDAP driver documentation.

Share
← Back to the Journal

More from the Journal