I'm struggling to figure out how to add ldap authentication to a webapp that I am writing to manage entries in the LDAP server itself.
I want users to be able to log into the webapp with their LDAP credentials and most importantly, bind with their credentials for subsequent LDAP queries.
The part that is tripping me up the most is this:
You're supposed to bind when needed, and unbind when you are done. So while I can create a $_SESSION["username"] object, I've seen no examples of a $_SESSION["password"] object for subsequent LDAP bind calls. That being said, how do I incorporate start_session() and use ldap_bind (Which requires the password) correctly if not by creating a $_SESSION["password"] object?
Example class I am working on:
<?php
session_start();
class LdapAuthenticator {
private $ldapConnection;
private string $ldapUri;
public function __construct(string $uri) {
$this->ldapUri = $uri;
$ldap_error_message = ldap_error($this->ldapConnection);
if($this->ldapConnection === false) {
throw new Exception($ldap_error_message);
}
}
public function connect(): bool {
$this->ldapConnection = ldap_connect($this->ldapUri);
if ($this->ldapConnection) {
ldap_set_option($this->ldapConnection, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->ldapConnection, LDAP_OPT_REFERRALS, 0);
return true;
}
return false;
}
public function bind(string $bind_dn, string $bind_pw): bool {
if (!isset($this->ldapConnection)) {
throw new Exception("LDAP connection not established. Call connect() first.");
}
$ldap_error_message = ldap_error($this->ldapConnection);
if (!empty($ldap_error_message) && $ldap_error_message !== "Success") {
if(ldap_get_option($this->ldapConnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
throw new Exception("$ldap_error_message - $extended_error");
} else {
throw new Exception("$ldap_error_message");
}
}
return ldap_bind($this->ldapConnection, $bind_dn, $bind_pw);
}
public function authenticate(string $username, string $password) {
// Attempt to bind with user credentials
if ($this->bind($username, $password) "uid={$username},{$this->baseDn}", $password) {
$_SESSION['ldap_username'] = $username;
// Optionally retrieve and store other user details
return true;
}
return false;
}
public function isLoggedIn() {
return isset($_SESSION['ldap_username']);
}
public function close(): void {
if (isset($this->ldapConnection)) {
ldap_unbind($this->ldapConnection);
}
}
public function getError(): string {
if (isset($this->ldapConnection)) {
return ldap_error($this->ldapConnection);
}
return "No LDAP connection.";
}
public function search(string $filter, string $settings) {
$search_result = ldap_search($this->ldapConnection, $settings, $filter);
$ldap_error_message = ldap_error($this->ldapConnection);
if (!empty($ldap_error_message) && $ldap_error_message !== "Success") {
if(ldap_get_option($this->ldapConnection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
throw new Exception("$ldap_error_message - $extended_error");
} else {
throw new Exception("$ldap_error_message");
}
}
return ldap_get_entries($this->ldapConnection, $search_result);
}
}
?>
Example page code I am trying to piece together:
<?
try {
$host = "ldaps://ldap.example.com";
$baseDn = "dc=example,dc=com";
$ldap = new LdapAuthenticator($host);
if ($ldap->connect()) {
if (!$ldap->bind($_POST["username"], $_POST['password'])) {
$htmlerror = $ldap->getError();
} else {
$htmlerror = $ldap->getError();
// Proceed with other LDAP operations (search, modify, etc.)
$entries = $ldap->search("(&(objectClass=nsPerson)(objectClass=posixAccount))", $baseDn);
}
$ldap->close();
} else {
echo "Failed to connect to LDAP server.\n";
}
} catch (Exception $e) {
echo "Error: " . $e->getMessage() . "\n";
}
if (isset($_POST['username']) && isset($_POST['password'])) {
if ($ldapSession->authenticate($_POST['username'], $_POST['password'])) {
header("Location: protected_page.php");
exit();
} else {
echo "Invalid credentials.";
}
}
if ($ldapSession->isLoggedIn()) {
echo "Welcome, " . $_SESSION['ldap_username'] . "!";
} else {
// Display login form
}
?>
You're supposed to bind when needed, and unbind when you are done
Same as any other TCP-based service. If you don't close the connection at the end, the PHP interpreter will do it for you eventually, but it's better to disconnect explicitly.
So while I can create a
$_SESSION["username"]object, I've seen no examples of a$_SESSION["password"]object for subsequent LDAP bind calls
Create one. If you need to store user credentials, then you have to store credentials. There's no good way around it. Take a look at how e.g. phpMyAdmin does it for MySQL – the user's database server credentials are just stored in a cookie, encrypted using a static configured key (it uses sodium_secret_box) – and that's the same thing as $_SESSION["encrypted_password"].
Some LDAP servers might be configured to allow impersonation, where you bind using the app's service account but specify a separate "authorization" username¹, thereby acting as that user instead of the service user – without having to specify the target user's password.
Support for this is relatively rare though, and in most cases you have no other choice but to literally store the user's password within the 'session' data.
¹ (If you're curious, this is either the $authz_id parameter for ldap_sasl_bind() – if SASL is used – or the LDAP_CONTROL_AUTHZID_REQUEST control for ldap_bind_ext() when doing a traditional password-based bind.)
Regarding $bind_dn: Don't hardcode assumptions that it will always be a fixed template. Sure, if the directory admins treat LDAP as a baroque version of YP/NIS, then it will. But in many cases – especially Active Directory or X.500 style directories – the user accounts might be arranged under different OUs, and/or have names that are not based on their UID.
The proper way to implement this is to 1) bind using a preconfigured "service account", 2) search for the username, verifying that exactly 1 entry is found (no more, no less), 3) remember that entry's DN and use it as the bind_dn from that point (storing it in the session if needed).