LDAP (Lightweight Directory Access Protocol) is an open protocol used for storing information about an organization and its users and assets. This protocol is used to communicate with a directory database to query, add or modify information.

LDAP authentication authenticates the user via LDAP server such as Active Directory. For such, user should have valid directory record in LDAP server to get authorized to access certain system or services.

Since LDAP is an open protocol, there are many different implementations available, the most prominent probably Microsoft Active Directory and OpenLDAP. We will use 389 Directory Service as the LDAP Server and the directory is under domain ldap.hanan.my.id as an example.

I’m using Fedora Cloud for installing the LDAP server, you may consider using other operating system you like. Simply install the 389ds package on Fedora: dnf install 389-ds-base and will access it through Cockpit: dnf install cockpit-389-ds.

A 389ds instance can be created through dscreate from the inf file. Create the instance.inf file with basis configuration:

[general]
full_machine_name = ldap.hanan.my.id
start = True

[slapd]
instance_name = localhost
root_password = mysecret
port = 389
secure_port = 636
self_sign_cert = False

[backend-userroot]
sample_entries = yes
suffix = dc=ldap,dc=hanan,dc=my,dc=id

Change the highlighted lines (2,7,14) according to your configuration. Run command dscreate for creating the instance: dscreate from-file instance.inf

During the installation a systemd unit file called dirsrv@INSTANCE_NAME.service is created and enabled so the instance is automatically started on boot. You may check the systemd status:

~# systemctl status [email protected][email protected] - 389 Directory Server localhost.
     Loaded: loaded (/usr/lib/systemd/system/[email protected]; enabled; preset: disabled)
    Drop-In: /usr/lib/systemd/system/service.d
             └─10-timeout-abort.conf
             /usr/lib/systemd/system/[email protected]
             └─custom.conf
     Active: active (running) since Sat 2024-01-27 15:02:08 UTC; 3min 22s ago
    Process: 11500 ExecStartPre=/usr/libexec/dirsrv/ds_systemd_ask_password_acl /etc/dirsrv/slapd-localhost/dse.ldif (code=exited, status=0/SUCCESS)
    Process: 11506 ExecStartPre=/usr/libexec/dirsrv/ds_selinux_restorecon.sh /etc/dirsrv/slapd-localhost/dse.ldif (code=exited, status=0/SUCCESS)
   Main PID: 11512 (ns-slapd)
     Status: "slapd started: Ready to process requests"
      Tasks: 29 (limit: 1087)
     Memory: 95.2M
        CPU: 1.054s
     CGroup: /system.slice/system-dirsrv.slice/[email protected]
             └─11512 /usr/sbin/ns-slapd -D /etc/dirsrv/slapd-localhost -i /run/dirsrv/slapd-localhost.pid

From now, we use Cockpit to manage 389ds, activate the cockpit service: systemctl enable --now cockpit.socket

Let’s add a new User in with the basic information in the ou=people tree:


For implementing LDAP authentication in Laravel, we pass user credentials to LDAP server for validation via authentication service. Further authorization is handled by Laravel itself after authentication.

I have a fresh installation of Laravel with Laravel UI authentication scaffolding. You should add the LdapRecord-Laravel library with composer: composer require directorytree/ldaprecord-laravel and publish the configuration: php artisan vendor:publish --provider="LdapRecord\Laravel\LdapServiceProvider".

The ldap.php file will then be created inside your applications config, directory. You may set up inside the configuration file, or add some configuration in your .env file:

LDAP_LOGGING=true
LDAP_CONNECTION=default
LDAP_HOST=ldap.hanan.my.id
LDAP_USERNAME="cn=Directory Manager"
LDAP_PASSWORD=mysecret
LDAP_PORT=389
LDAP_BASE_DN="dc=ldap,dc=hanan,dc=my,dc=id"
LDAP_TIMEOUT=5
LDAP_SSL=false
LDAP_TLS=false
LDAP_SASL=false

Once you have your connection configured, run a quick test to make sure they’ve been set up properly: php artisan ldap:test

You’re ready to start running queries and operations on your LDAP server. First thing I did was simply checking a user LDAP credentials are valid. Open php artisan tinker and execute some command:

> $connection = LdapRecord\Container::getConnection('default');
> $connection->auth()->attempt('cn=Admin,ou=people,dc=ldap,dc=hanan,dc=my,dc=id', 'mysecret')
true

You may create the LDAP modal via the ldap:make:model command: php artisan ldap:make:model User. This will create a new LdapRecord model inside your application in the app/Ldap folder. This is the example of my LdapRecord model:

# app/Ldap/User.php
<?php

namespace App\Ldap;

use LdapRecord\Models\Model;
use LdapRecord\Models\Concerns\CanAuthenticate;
use Illuminate\Contracts\Auth\Authenticatable;
use LdapRecord\Query\Model\Builder;

class User extends Model implements Authenticatable
{
    use CanAuthenticate;
    
    /**
     * The object classes of the LDAP model.
     */
    public static array $objectClasses = [
        "nsPerson",
        "nsAccount"
    ];

    protected string $guidKey = 'entryUUID';

    protected static function boot(): void
    {
        parent::boot();

        static::addGlobalScope('people', function (Builder $builder) {
            $builder->in('ou=people,{base}');
        });
    }
}

To begin querying your model, you can statically call query methods off of the model:

use App\Ldap\User;
..
User::where('mail', '[email protected]')->first();

In the boot() method, the scope is added to utilize query filters in the organizational unit ou=people. Any search queries that are performed on User model will be properly scoped. Meaning when you search '[email protected]', it will perform search in scope of ou=people only.

The new object also can be created from the User model and save it into the directory:

$ldapUser = new User([
    'cn'=>'Eve', 
    'displayName'=>'Eve', 
    'mail'=>'[email protected]', 
    'userPassword'=>'mysecret'
]);
$ldapUser->inside('ou=people,dc=ldap,dc=hanan,dc=my,dc=id');
$ldapUser->save()

There are two ways to authenticate LDAP users into Laravel application. If you have an application that doesn’t require any user data to be synchronized to your database, then you can utilize plain LDAP authentication. Otherwise, you may use the synchronized database LDAP Authentication method, meaning that an LDAP user which successfully passes LDAP authentication will be created & synchronized to your local application’s database. In my case, I chose the second option.

Let’s configure the authentication provider inside config/auth.php:

'providers' => [
    ...
    'users' => [
        'driver' => 'ldap',
        'model' => App\Ldap\User,
        'rules' => [],
        'scopes' => [],
        'database' => [
        'model' => App\Models\User::class,
        'sync_passwords' => true,
        'sync_attributes' => [
            'name' => 'cn',
            'email' => 'mail',
        ],
        'sync_existing' => [
            'email' => 'mail',
        ],
    ],
    ...
],

For this example application, we will authenticate our LDAP users with their email address using the LDAP attribute mail. Override the credentials method in the LoginController:

// app/Http/Controllers/Auth/LoginController.php

use Illuminate\Http\Request;

protected function credentials(Request $request)
{
    return [
        'mail' => $request->email,
        'password' => $request->password,
    ];
}

If a user attempts to log in with the above email address and this user does not exist inside your LDAP directory, then standard Eloquent authentication will be performed instead.

The sync_passwords option, the sync_attributes array and the sync_existing array in the authentication provider inside config/auth.php make the users database table to be synchronized when users successfully pass LDAP authentication.


Further information:

Leave a comment

Leave a Reply