Custom Identifier to login against other service (Active Directory in this case)

Hi,

I hope someone can help since I can’t figure this out :frowning:

CakePHP 4 has a LDAP authenticator, but it does not get group data and for some reason it doesn’t want to connect to my AD. As a result I did some searching and found LDAP record after some usage of the previous version (AdLdap2) and used it in a CakePHP 3.x project.

For some reason CakePHP 4 and the Authenticator plugin don’t want to do the same as I want :wink:

I did some testing if I can use LDAP record based on this controller. Lucky me… I can connect to the Active Directory and get the data needed (yes it is ugly code, it is only for testing):

<?php
declare(strict_types=1);

namespace App\Controller;

use LdapRecord\Container;
use LdapRecord\Auth\Events\Failed;

class TestController extends AppController
{
    public function index()
    {
        // https://ldaprecord.com/
        // https://ldaprecord.com/docs/core/v2/authentication#basic-authentication
        $username = 'test';
        $password = 'password';

        $connection = new \LdapRecord\Connection([
            'hosts' => ['masterdc.network.local'],
            'port' => 389,
            'base_dn' => 'dc=network,dc=local',
        ]);

        try {
            if ($connection->auth()->attempt($username, $password, $stayAuthenticated = true)) {
                // Successfully authenticated user.
                debug('loggedin');
            } else {
                // Username or password is incorrect.
                debug('nog access');
            }
        } catch (\LdapRecord\Auth\BindException $e) {
            $error = $e->getDetailedError()->getDiagnosticMessage();
        
            if (strpos($error, '532') !== false) {
                debug('Your password has expired.');
            } elseif (strpos($error, '533') !== false) {
                debug( 'Your account is disabled.');
            } elseif (strpos($error, '701') !== false) {
                debug( 'Your account has expired');
            } elseif (strpos($error, '775') !== false) {
                debug( 'Your account is locked.');
            }
        
            debug( 'Username or password is incorrect.');
        }

        $user = $connection->query()
            ->where('samaccountname', '=', $username)
            ->firstOrFail();

        // Get the groups of the user.
        $groups = $user['memberof'];

        debug($groups);
        debug($this->cleanGroups($groups));

    }
    
    protected function cleanGroups($groups)
    {
        $result = [];

        if($groups['count'] === 0) {
            return $result;
        }

        unset($groups['count']);

        foreach ($groups as $group) {
            $parts = explode(',', $group);

            foreach ($parts as $part) {
                if (substr($part, 0, 3) == 'CN=') {
                    $result[] = strtolower(substr($part, 3));
                    break;
                }
            }
        }

        return $result;
    }

}

Now… I want to change the code above to a Identifier so users can login based on the Active Directory and their group policy. So I’ve added the following in getAuthenticationService() in my Application.php:

$service = new AuthenticationService();

$service->setConfig([
                'unauthenticatedRedirect' => '/',
            ]);

            $service->loadAuthenticator('Authentication.Session');
            $service->loadAuthenticator('Authentication.Form', [
                'fields' => [
                    IdentifierInterface::CREDENTIAL_USERNAME => 'user',
                    IdentifierInterface::CREDENTIAL_PASSWORD => 'password',
                ],
            ]);

            $service->loadIdentifier('Ldap2', [
                'fields' => [
                    IdentifierInterface::CREDENTIAL_USERNAME => 'username',
                    IdentifierInterface::CREDENTIAL_PASSWORD => 'passwd',
                ],
                'hosts' => ['masterdc.network.local'],
                'baseDN' => 'dc=network,dc=local',
                'port' => 389,
                'bindDN' => function ($username) {
                    // validate $username here before embedding it in the DN string!
                    return "$username@network.local";
                },
            ]);

            return $service;

After that I’ve added the file Ldap2Identifier.php in ./src/Identifier/Ldap2Identifier.php with the following code:

<?php
declare(strict_types=1);

namespace App\Identifier;

use Authentication\Identifier\AbstractIdentifier;

use ArrayAccess;
use ArrayObject;
use InvalidArgumentException;
use RuntimeException;


class Ldap2Identifier extends AbstractIdentifier
{

    protected $_defaultConfig = [
        'fields' => [
            self::CREDENTIAL_USERNAME => 'username',
            self::CREDENTIAL_PASSWORD => 'password',
        ],
        'port' => 389,
    ];

    protected $_errors = [];
    private $_connection;

    public function __construct(array $config = [])
    {
        parent::__construct($config);
        $this->_checkLdapConfig();
    }

    protected function _checkLdapConfig(): void
    {
        if (!isset($this->_config['hosts']) || !is_array($this->_config['hosts'])) {
            throw new RuntimeException('Config `hosts` is not set or isnot an array.');
        }

        if (!isset($this->_config['baseDN'])) {
            throw new RuntimeException('Config `baseDN` is not set.');
        }

        if (!is_callable($this->_config['bindDN'])) {
            throw new InvalidArgumentException(sprintf(
                'The `bindDN` config is not a callable. Got `%s` instead.',
                gettype($this->_config['bindDN'])
            ));
        }
    }

    public function identify(array $data)
    {
        $fields = $this->getConfig('fields');

        if(isset($data[$fields[self::CREDENTIAL_USERNAME]]) && isset($data[$fields[self::CREDENTIAL_PASSWORD]]))
        {
            $this->_connectLdap();

            return $this->_bindUser(
                $data[$fields[self::CREDENTIAL_USERNAME]],
                $data[$fields[self::CREDENTIAL_PASSWORD]]
            );
        }

        return null;
    }

    protected function _connectLdap(): void
    {
        $config = $this->getConfig();

        $this->_connection = new \LdapRecord\Connection([
            'hosts' => $config['hosts'],
            'port' => $config['port'],
            'base_dn' => $config['baseDN'],
        ]);
    }

    protected function _bindUser(string $username, string $password): ?ArrayAccess
    {
        $config = $this->getConfig();
        try {
            if ($this->_connection->auth()->attempt($config['bindDN']($username), $password, $stayAuthenticated = true)) {
                // Successfully authenticated user.
                return new ArrayObject([
                    $config['fields'][self::CREDENTIAL_USERNAME] => $username,
                ]);
            }
        } catch (\LdapRecord\Auth\BindException $e) {
            $this->_handleLdapError($e->getDetailedError()->getDiagnosticMessage());
        }

        return null;
    }

    public function getErrors(): array
    {
        return $this->_errors;
    }

    protected function _handleLdapError(string $message): void
    {
        if (strpos($message, '532') !== false) {
            $this->_errors[] = 'Your password has expired.';
        } elseif (strpos($message, '533') !== false) {
            $this->_errors[] = 'Your account is disabled.';
        } elseif (strpos($message, '701') !== false) {
            $this->_errors[] = 'Your account has expired';
        } elseif (strpos($message, '775') !== false) {
            $this->_errors[] = 'Your account is locked.';
        }
    
        $this->_errors[] = 'Username or password is incorrect.';
    }
}

This file is loaded, can access break points in __construct(), but when I login/submit the login for I never get in identify(). I’m always directly redirect to the same page with the error “no access”.

I’m missing something, but I have no idea what… It is kind for frustrating :wink:

Looked at this docs: Quick Start - 2.x and asked my friend Google, but for some reason I can’t find the (probably) little thing that is missing…

I there someone with experience with custom Identifiers to login? And… knows what I’m missing here?

Thanks!

Argh… finally I had some time to dive into this again…

As it turned out… the username and password field where not matching the definition in the view. :man_facepalming: