DEV Community

Cover image for Testing LDAP Authentication with PHP: A Practical CLI Tool
Nasrul Hazim Bin Mohamad
Nasrul Hazim Bin Mohamad

Posted on

Testing LDAP Authentication with PHP: A Practical CLI Tool

Working with LDAP/Active Directory authentication can be frustrating. When credentials fail, you’re often left with cryptic error codes and little guidance on what went wrong. To make this easier, I built a simple PHP command-line tool that lets you test LDAP connections and user authentication with detailed diagnostics.

Whether you’re troubleshooting an intranet login, integrating LDAP into a PHP application, or just verifying that your Active Directory setup is working, this tool gives you a clear picture of what’s happening behind the scenes.


🚀 Why This Tool?

LDAP is powerful but notoriously tricky to debug. Typical problems include:

  • Wrong username format (UPN vs. DN vs. DOMAIN\username)
  • Invalid credentials (error 49)
  • No such object (error 32)
  • TLS/SSL certificate issues
  • Firewall or network problems

This tool aims to diagnose each step of the process:

  1. Test basic network connectivity
  2. Check if the LDAP server responds
  3. Attempt anonymous and authenticated binds
  4. Show supported LDAP versions and SASL mechanisms
  5. Suggest alternative username formats if authentication fails

⚙️ Requirements

  • PHP with LDAP extension (php-ldap)
  • Network access to your LDAP/AD server

Install the LDAP extension if you don’t have it:

# Ubuntu/Debian
sudo apt-get install php-ldap

# CentOS/RHEL
sudo yum install php-ldap
Enter fullscreen mode Exit fullscreen mode

Script

<?php
error_reporting(E_ALL);
ini_set('display_errors', 1);

// Function to read password securely (hidden input)
function readPasswordHidden($prompt = "Password: ") {
    echo $prompt;

    // Check if we're on Windows or Unix-like system
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
        // Windows - use PowerShell for hidden input
        $password = rtrim(shell_exec('powershell -Command "$p = Read-Host -AsSecureString; [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($p))"'));
    } else {
        // Unix/Linux/Mac - use stty to hide input
        system('stty -echo');
        $password = rtrim(fgets(STDIN));
        system('stty echo');
        echo "\n"; // Add newline since it was hidden
    }

    return $password;
}

// Check command line arguments
if ($argc < 2) {
    echo "Usage: php {$argv[0]} <ldap-dn> [ldap-host]\n";
    echo "Examples:\n";
    echo "  php {$argv[0]} \"CN=Administrator,CN=Users,DC=example,DC=local\"\n";
    echo "  php {$argv[0]} \"Administrator@example.local\" \"ldap://192.168.1.100:389\"\n";
    echo "  php {$argv[0]} \"DOMAIN\\\\username\"\n";
    exit(1);
}

$ldap_dn = $argv[1];
$ldap_host = isset($argv[2]) ? $argv[2] : "ldap://ip-or-domain:port";

// Read password securely
$ldap_pass = readPasswordHidden("Enter password for '$ldap_dn': ");

if (empty($ldap_pass)) {
    die("❌ Password cannot be empty!\n");
}

echo "\n🔍 LDAP Authentication Test (Enhanced Debug)\n";
echo "============================================\n";
echo "Host: $ldap_host\n";
echo "Bind DN: $ldap_dn\n";
echo "Password: " . str_repeat('*', strlen($ldap_pass)) . " (" . strlen($ldap_pass) . " chars)\n\n";

// Check if LDAP extension is loaded
if (!extension_loaded('ldap')) {
    die("❌ LDAP extension is not loaded!\n");
}
echo "✅ LDAP extension is loaded\n";

// Test basic connectivity first
echo "\n➡ Testing basic network connectivity...\n";
$host_parts = parse_url($ldap_host);
$host_ip = $host_parts['host'];
$host_port = isset($host_parts['port']) ? $host_parts['port'] : 389;

echo "   Attempting to connect to $host_ip:$host_port...\n";
$socket = @fsockopen($host_ip, $host_port, $errno, $errstr, 5);
if ($socket) {
    echo "✅ Network connection successful\n";
    fclose($socket);
} else {
    echo "❌ Network connection failed: $errstr ($errno)\n";
    echo "   Check if LDAP server is running and accessible\n";
    exit(1);
}

// Optional: disable TLS cert checks for self-signed (testing only)
putenv('LDAPTLS_REQCERT=never');
ldap_set_option(NULL, LDAP_OPT_X_TLS_REQUIRE_CERT, LDAP_OPT_X_TLS_NEVER);

// Connect with more detailed error handling
echo "\n➡ Creating LDAP connection...\n";
$ldap_conn = ldap_connect($ldap_host);

if (!$ldap_conn) {
    die("❌ Could not create LDAP connection handle\n");
} else {
    echo "✅ LDAP connection resource created\n";
}

// Set options before binding
echo "➡ Setting LDAP options...\n";
if (!ldap_set_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, 3)) {
    echo "⚠ Failed to set LDAP protocol version to 3\n";
}
if (!ldap_set_option($ldap_conn, LDAP_OPT_REFERRALS, 0)) {
    echo "⚠ Failed to disable referrals\n";
}

// Set network timeout
ldap_set_option($ldap_conn, LDAP_OPT_NETWORK_TIMEOUT, 10);

// Dump current options for debugging
$version = null;
$referrals = null;
$timeout = null;
ldap_get_option($ldap_conn, LDAP_OPT_PROTOCOL_VERSION, $version);
ldap_get_option($ldap_conn, LDAP_OPT_REFERRALS, $referrals);
ldap_get_option($ldap_conn, LDAP_OPT_NETWORK_TIMEOUT, $timeout);
echo "ℹ LDAP protocol version: $version\n";
echo "ℹ Referrals enabled: " . ($referrals ? "Yes" : "No") . "\n";
echo "ℹ Network timeout: {$timeout}s\n\n";

// Try anonymous bind first to test basic LDAP connectivity
echo "➡ Testing anonymous bind...\n";
if (@ldap_bind($ldap_conn)) {
    echo "✅ Anonymous bind successful - LDAP server is responding\n";

    // Try to read RootDSE with anonymous access
    echo "➡ Reading RootDSE with anonymous access...\n";
    $sr = @ldap_read($ldap_conn, "", "(objectClass=*)", ["supportedLDAPVersion", "defaultNamingContext", "supportedSASLMechanisms"]);
    if ($sr) {
        $entries = ldap_get_entries($ldap_conn, $sr);
        if ($entries['count'] > 0) {
            echo "✅ RootDSE accessible:\n";
            if (isset($entries[0]["supportedldapversion"])) {
                echo "   Supported LDAP versions: " . implode(", ", array_slice($entries[0]["supportedldapversion"], 1)) . "\n";
            }
            if (isset($entries[0]["defaultnamingcontext"][0])) {
                echo "   Default naming context: " . $entries[0]["defaultnamingcontext"][0] . "\n";
            }
            if (isset($entries[0]["supportedsaslmechanisms"])) {
                echo "   Supported SASL mechanisms: " . implode(", ", array_slice($entries[0]["supportedsaslmechanisms"], 1)) . "\n";
            }
        }
    }
} else {
    echo "⚠ Anonymous bind failed: " . ldap_error($ldap_conn) . "\n";
    echo "   This might be expected if anonymous access is disabled\n";
}

// Attempt authenticated bind
echo "\n➡ Attempting authenticated bind...\n";
echo "   DN: $ldap_dn\n";
echo "   Password length: " . strlen($ldap_pass) . " characters\n";

// Test with provided credentials
$bind_result = @ldap_bind($ldap_conn, $ldap_dn, $ldap_pass);

if ($bind_result) {
    echo "✅ Authentication successful!\n";

    // Test a simple search to verify the connection works
    echo "➡ Testing search functionality...\n";
    $search_base = "CN=Users,DC=example,DC=local";
    $search_result = @ldap_search($ldap_conn, $search_base, "(objectClass=user)", ["cn", "sAMAccountName"], 0, 5);

    if ($search_result) {
        $entries = ldap_get_entries($ldap_conn, $search_result);
        echo "✅ Search successful - found {$entries['count']} user(s)\n";

        // Display some user information if found
        if ($entries['count'] > 0) {
            echo "   Sample users found:\n";
            for ($i = 0; $i < min(3, $entries['count']); $i++) {
                $cn = isset($entries[$i]['cn'][0]) ? $entries[$i]['cn'][0] : 'N/A';
                $sam = isset($entries[$i]['samaccountname'][0]) ? $entries[$i]['samaccountname'][0] : 'N/A';
                echo "   - CN: $cn, sAMAccountName: $sam\n";
            }
        }
    } else {
        echo "⚠ Search failed: " . ldap_error($ldap_conn) . "\n";
        echo "   This might be due to insufficient permissions or incorrect base DN\n";
    }

} else {
    $err_no  = ldap_errno($ldap_conn);
    $err_msg = ldap_error($ldap_conn);

    echo "❌ Authentication failed!\n";
    echo "   LDAP Error Code: $err_no\n";
    echo "   LDAP Error Message: $err_msg\n";

    // Try to get more detailed diagnostic information
    $diag_msg = null;
    if (ldap_get_option($ldap_conn, LDAP_OPT_DIAGNOSTIC_MESSAGE, $diag_msg) && !empty($diag_msg)) {
        echo "   Diagnostic Message: $diag_msg\n";
    }

    echo "\n🔧 Troubleshooting suggestions:\n";

    // Common error codes and suggestions
    switch ($err_no) {
        case 49: // LDAP_INVALID_CREDENTIALS
            echo "   - Invalid credentials (error 49)\n";
            echo "     → Verify username and password\n";
            echo "     → Check if account is locked or disabled\n";
            echo "     → Verify DN format matches your AD structure\n";
            echo "     → Try with sAMAccountName format: Administrator\n";
            break;
        case 32: // LDAP_NO_SUCH_OBJECT
            echo "   - Object not found (error 32)\n";
            echo "     → Verify the DN path exists\n";
            echo "     → Check your domain components (DC=example,DC=local)\n";
            break;
        case 34: // LDAP_INVALID_DN_SYNTAX
            echo "   - Invalid DN syntax (error 34)\n";
            echo "     → Check DN format and escaping\n";
            break;
        case -1: // Connection issues
            echo "   - Connection problem (error -1)\n";
            echo "     → Check network connectivity\n";
            echo "     → Verify LDAP server is running\n";
            echo "     → Check firewall settings\n";
            break;
        default:
            echo "   - See LDAP error code documentation for error $err_no\n";
    }

    echo "\n🔧 Alternative authentication formats to consider:\n";
    echo "   1. Distinguished Name: CN=username,CN=Users,DC=domain,DC=com\n";
    echo "   2. sAMAccountName: username\n";
    echo "   3. User Principal Name: username@domain.com\n";
    echo "   4. Legacy format: DOMAIN\\username\n";

    // Extract potential alternative formats from the provided DN
    $alternative_dns = [];

    // If it looks like a DN, try extracting sAMAccountName-like format
    if (strpos($ldap_dn, 'CN=') !== false) {
        preg_match('/CN=([^,]+)/', $ldap_dn, $matches);
        if (isset($matches[1])) {
            $alternative_dns[] = $matches[1]; // Just the username
        }
    }

    // If it has domain info, try other formats
    if (strpos($ldap_dn, '@') !== false) {
        $parts = explode('@', $ldap_dn);
        if (count($parts) == 2) {
            $alternative_dns[] = $parts[0]; // Just username without domain
            $domain_upper = strtoupper(explode('.', $parts[1])[0]);
            $alternative_dns[] = $domain_upper . '\\' . $parts[0]; // DOMAIN\username
        }
    }

    if (!empty($alternative_dns)) {
        echo "\n➡ Suggested alternative formats based on your input:\n";
        foreach (array_unique($alternative_dns) as $alt_dn) {
            if ($alt_dn !== $ldap_dn) {
                echo "   - Try: php {$argv[0]} \"$alt_dn\"\n";
            }
        }
    }
}

// Clear password from memory for security
$ldap_pass = str_repeat('0', strlen($ldap_pass));
unset($ldap_pass);

ldap_unbind($ldap_conn);
echo "\n✅ Test complete.\n";
?>
Enter fullscreen mode Exit fullscreen mode

💻 Usage

Save the script as ldap-test.php, then run:

php ldap-test.php "<username>" [ldap-host]
Enter fullscreen mode Exit fullscreen mode

You’ll be prompted for a password (securely hidden input).

Common Examples

# Active Directory - User Principal Name
php ldap-test.php "admin@company.com"

# Distinguished Name
php ldap-test.php "CN=Administrator,CN=Users,DC=company,DC=local"

# Simple username
php ldap-test.php "administrator"

# Legacy format
php ldap-test.php "COMPANY\\admin"

# Custom server
php ldap-test.php "admin@company.com" "ldap://192.168.1.100:389"

# Secure LDAP (LDAPS)
php ldap-test.php "admin@company.com" "ldaps://dc01.company.com:636"
Enter fullscreen mode Exit fullscreen mode

🔍 What It Does

Here’s what happens when you run the tool:

  1. Connectivity Check – Verifies the server is reachable on the given host/port.
  2. LDAP Extension Check – Ensures PHP has LDAP support.
  3. Anonymous Bind Test – Confirms if the server allows basic queries.
  4. RootDSE Read – Retrieves server info (supported LDAP versions, naming context).
  5. Authenticated Bind – Attempts login with your credentials.
  6. Search Test – Runs a simple search to validate permissions.
  7. Error Diagnostics – If authentication fails, shows error code, message, and troubleshooting tips.

🧰 Common Errors & Fixes

Error Meaning Fix
49 Invalid credentials Try different username format, verify password, check if account is locked/disabled
32 No such object Verify DN path (CN=Users,DC=example,DC=local)
34 Invalid DN syntax Fix your DN formatting
-1 Connection issue Check firewall, server status, network reachability

If authentication fails, the tool also suggests alternative formats automatically, such as:

  • username@domain.com
  • DOMAIN\username
  • CN=username,CN=Users,DC=domain,DC=com
  • Just username

📝 Example Output

🔍 LDAP Authentication Test (Enhanced Debug)
============================================
Host: ldap://192.168.1.100:389
Bind DN: admin@company.com
Password: ******** (8 chars)

✅ LDAP extension is loaded
✅ Network connection successful
✅ LDAP connection resource created
ℹ LDAP protocol version: 3
ℹ Referrals enabled: No
ℹ Network timeout: 10s

➡ Attempting authenticated bind...
✅ Authentication successful!
✅ Search successful - found 3 user(s)
   - CN: Administrator, sAMAccountName: administrator
Enter fullscreen mode Exit fullscreen mode

If something goes wrong, you’ll get detailed output like:

❌ Authentication failed!
   LDAP Error Code: 49
   LDAP Error Message: Invalid credentials
   Diagnostic Message: 80090308: LdapErr: DSID-0C0903AA, comment: AcceptSecurityContext error

🔧 Troubleshooting suggestions:
   - Verify username and password
   - Check account lockout
   - Try alternative login formats (DOMAIN\user, user@domain.com)
Enter fullscreen mode Exit fullscreen mode

✅ Conclusion

This LDAP Authentication Test Tool helps take the guesswork out of LDAP/Active Directory troubleshooting. Instead of chasing vague errors, you get:

  • Clear diagnostics
  • Alternative login format suggestions
  • Confidence that your PHP-LDAP setup is working

If you’re integrating LDAP into PHP applications or just need a quick way to validate credentials, this script can save you hours of debugging.

Top comments (0)