Hi,
I hope someone can help since I can’t figure this out
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
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
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!