[Security] Added LDAP support to Authenticator system
This commit is contained in:
parent
017420b511
commit
20962e604a
@ -53,6 +53,7 @@ class UnusedTagsPass implements CompilerPassInterface
|
|||||||
'kernel.fragment_renderer',
|
'kernel.fragment_renderer',
|
||||||
'kernel.locale_aware',
|
'kernel.locale_aware',
|
||||||
'kernel.reset',
|
'kernel.reset',
|
||||||
|
'ldap',
|
||||||
'mailer.transport_factory',
|
'mailer.transport_factory',
|
||||||
'messenger.bus',
|
'messenger.bus',
|
||||||
'messenger.message_handler',
|
'messenger.message_handler',
|
||||||
|
@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler;
|
||||||
|
|
||||||
|
use Symfony\Component\DependencyInjection\Argument\ServiceClosureArgument;
|
||||||
|
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\DependencyInjection\Definition;
|
||||||
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
use Symfony\Component\DependencyInjection\ServiceLocator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
class RegisterLdapLocatorPass implements CompilerPassInterface
|
||||||
|
{
|
||||||
|
public function process(ContainerBuilder $container)
|
||||||
|
{
|
||||||
|
$definition = $container->setDefinition('security.ldap_locator', new Definition(ServiceLocator::class));
|
||||||
|
|
||||||
|
$locators = [];
|
||||||
|
foreach ($container->findTaggedServiceIds('ldap') as $serviceId => $tags) {
|
||||||
|
$locators[$serviceId] = new ServiceClosureArgument(new Reference($serviceId));
|
||||||
|
}
|
||||||
|
|
||||||
|
$definition->addArgument($locators);
|
||||||
|
}
|
||||||
|
}
|
@ -27,6 +27,8 @@ use Symfony\Component\Security\Core\Exception\LogicException;
|
|||||||
*/
|
*/
|
||||||
class FormLoginLdapFactory extends FormLoginFactory
|
class FormLoginLdapFactory extends FormLoginFactory
|
||||||
{
|
{
|
||||||
|
use LdapFactoryTrait;
|
||||||
|
|
||||||
protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId)
|
protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId)
|
||||||
{
|
{
|
||||||
$provider = 'security.authentication.provider.ldap_bind.'.$id;
|
$provider = 'security.authentication.provider.ldap_bind.'.$id;
|
||||||
|
@ -28,6 +28,8 @@ use Symfony\Component\Security\Core\Exception\LogicException;
|
|||||||
*/
|
*/
|
||||||
class HttpBasicLdapFactory extends HttpBasicFactory
|
class HttpBasicLdapFactory extends HttpBasicFactory
|
||||||
{
|
{
|
||||||
|
use LdapFactoryTrait;
|
||||||
|
|
||||||
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
|
||||||
{
|
{
|
||||||
$provider = 'security.authentication.provider.ldap_bind.'.$id;
|
$provider = 'security.authentication.provider.ldap_bind.'.$id;
|
||||||
|
@ -24,6 +24,8 @@ use Symfony\Component\Security\Core\Exception\LogicException;
|
|||||||
*/
|
*/
|
||||||
class JsonLoginLdapFactory extends JsonLoginFactory
|
class JsonLoginLdapFactory extends JsonLoginFactory
|
||||||
{
|
{
|
||||||
|
use LdapFactoryTrait;
|
||||||
|
|
||||||
public function getKey()
|
public function getKey()
|
||||||
{
|
{
|
||||||
return 'json-login-ldap';
|
return 'json-login-ldap';
|
||||||
|
@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
|
||||||
|
|
||||||
|
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\DependencyInjection\Definition;
|
||||||
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener;
|
||||||
|
use Symfony\Component\Ldap\Security\LdapAuthenticator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A trait decorating the authenticator with LDAP functionality.
|
||||||
|
*
|
||||||
|
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
trait LdapFactoryTrait
|
||||||
|
{
|
||||||
|
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
|
||||||
|
{
|
||||||
|
$key = str_replace('-', '_', $this->getKey());
|
||||||
|
if (!class_exists(LdapAuthenticator::class)) {
|
||||||
|
throw new \LogicException(sprintf('The "%s" authenticator requires the "symfony/ldap" package version "5.1" or higher.', $key));
|
||||||
|
}
|
||||||
|
|
||||||
|
$authenticatorId = parent::createAuthenticator($container, $firewallName, $config, $userProviderId);
|
||||||
|
|
||||||
|
$container->setDefinition('security.listener.'.$key.'.'.$firewallName, new Definition(CheckLdapCredentialsListener::class))
|
||||||
|
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
|
||||||
|
->addArgument(new Reference('security.ldap_locator'))
|
||||||
|
;
|
||||||
|
|
||||||
|
$ldapAuthenticatorId = 'security.authenticator.'.$key.'.'.$firewallName;
|
||||||
|
$definition = $container->setDefinition($ldapAuthenticatorId, new Definition(LdapAuthenticator::class))
|
||||||
|
->setArguments([
|
||||||
|
new Reference($authenticatorId),
|
||||||
|
$config['service'],
|
||||||
|
$config['dn_string'],
|
||||||
|
$config['search_dn'],
|
||||||
|
$config['search_password'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (!empty($config['query_string'])) {
|
||||||
|
if ('' === $config['search_dn'] || '' === $config['search_password']) {
|
||||||
|
throw new InvalidConfigurationException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$definition->addArgument($config['query_string']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $ldapAuthenticatorId;
|
||||||
|
}
|
||||||
|
}
|
@ -15,6 +15,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddExpressionLang
|
|||||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass;
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass;
|
||||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass;
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSessionDomainConstraintPass;
|
||||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass;
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterCsrfTokenClearingLogoutHandlerPass;
|
||||||
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterLdapLocatorPass;
|
||||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\RegisterTokenUsageTrackingPass;
|
||||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory;
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\AnonymousFactory;
|
||||||
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory;
|
use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\CustomAuthenticatorFactory;
|
||||||
@ -73,6 +74,7 @@ class SecurityBundle extends Bundle
|
|||||||
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING);
|
$container->addCompilerPass(new AddSessionDomainConstraintPass(), PassConfig::TYPE_BEFORE_REMOVING);
|
||||||
$container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass());
|
$container->addCompilerPass(new RegisterCsrfTokenClearingLogoutHandlerPass());
|
||||||
$container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200);
|
$container->addCompilerPass(new RegisterTokenUsageTrackingPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, 200);
|
||||||
|
$container->addCompilerPass(new RegisterLdapLocatorPass());
|
||||||
|
|
||||||
$container->addCompilerPass(new AddEventAliasesPass([
|
$container->addCompilerPass(new AddEventAliasesPass([
|
||||||
AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS,
|
AuthenticationSuccessEvent::class => AuthenticationEvents::AUTHENTICATION_SUCCESS,
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
CHANGELOG
|
CHANGELOG
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
5.1.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
* Added `Security\LdapBadge`, `Security\LdapAuthenticator` and `Security\CheckLdapCredentialsListener` to integrate with the authenticator Security system
|
||||||
|
|
||||||
5.0.0
|
5.0.0
|
||||||
-----
|
-----
|
||||||
|
|
||||||
|
@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Ldap\Security;
|
||||||
|
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||||
|
use Symfony\Component\Ldap\Exception\ConnectionException;
|
||||||
|
use Symfony\Component\Ldap\LdapInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\LogicException;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
|
||||||
|
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verifies password credentials using an LDAP service whenever the
|
||||||
|
* LdapBadge is attached to the Security passport.
|
||||||
|
*
|
||||||
|
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||||
|
*/
|
||||||
|
class CheckLdapCredentialsListener implements EventSubscriberInterface
|
||||||
|
{
|
||||||
|
private $ldapLocator;
|
||||||
|
|
||||||
|
public function __construct(ContainerInterface $ldapLocator)
|
||||||
|
{
|
||||||
|
$this->ldapLocator = $ldapLocator;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onCheckPassport(CheckPassportEvent $event)
|
||||||
|
{
|
||||||
|
$passport = $event->getPassport();
|
||||||
|
if (!$passport->hasBadge(LdapBadge::class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var LdapBadge $ldapBadge */
|
||||||
|
$ldapBadge = $passport->getBadge(LdapBadge::class);
|
||||||
|
if ($ldapBadge->isResolved()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$passport instanceof UserPassportInterface || !$passport->hasBadge(PasswordCredentials::class)) {
|
||||||
|
throw new \LogicException(sprintf('LDAP authentication requires a passport containing a user and password credentials, authenticator "%s" does not fulfill these requirements.', \get_class($event->getAuthenticator())));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var PasswordCredentials $passwordCredentials */
|
||||||
|
$passwordCredentials = $passport->getBadge(PasswordCredentials::class);
|
||||||
|
if ($passwordCredentials->isResolved()) {
|
||||||
|
throw new \LogicException('LDAP authentication password verification cannot be completed because something else has already resolved the PasswordCredentials.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->ldapLocator->has($ldapBadge->getLdapServiceId())) {
|
||||||
|
throw new \LogicException(sprintf('Cannot check credentials using the "%s" ldap service, as such service is not found. Did you maybe forget to add the "ldap" service tag to this service?', $ldapBadge->getLdapServiceId()));
|
||||||
|
}
|
||||||
|
|
||||||
|
$presentedPassword = $passwordCredentials->getPassword();
|
||||||
|
if ('' === $presentedPassword) {
|
||||||
|
throw new BadCredentialsException('The presented password cannot be empty.');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var LdapInterface $ldap */
|
||||||
|
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
|
||||||
|
try {
|
||||||
|
if ($ldapBadge->getQueryString()) {
|
||||||
|
if ('' !== $ldapBadge->getSearchDn() && '' !== $ldapBadge->getSearchPassword()) {
|
||||||
|
$ldap->bind($ldapBadge->getSearchDn(), $ldapBadge->getSearchPassword());
|
||||||
|
} else {
|
||||||
|
throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.');
|
||||||
|
}
|
||||||
|
$username = $ldap->escape($passport->getUser()->getUsername(), '', LdapInterface::ESCAPE_FILTER);
|
||||||
|
$query = str_replace('{username}', $username, $ldapBadge->getQueryString());
|
||||||
|
$result = $ldap->query($ldapBadge->getDnString(), $query)->execute();
|
||||||
|
if (1 !== $result->count()) {
|
||||||
|
throw new BadCredentialsException('The presented username is invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$dn = $result[0]->getDn();
|
||||||
|
} else {
|
||||||
|
$username = $ldap->escape($passport->getUser()->getUsername(), '', LdapInterface::ESCAPE_DN);
|
||||||
|
$dn = str_replace('{username}', $username, $ldapBadge->getDnString());
|
||||||
|
}
|
||||||
|
|
||||||
|
$ldap->bind($dn, $presentedPassword);
|
||||||
|
} catch (ConnectionException $e) {
|
||||||
|
throw new BadCredentialsException('The presented password is invalid.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$passwordCredentials->markResolved();
|
||||||
|
$ldapBadge->markResolved();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function getSubscribedEvents(): array
|
||||||
|
{
|
||||||
|
return [CheckPassportEvent::class => ['onCheckPassport', 144]];
|
||||||
|
}
|
||||||
|
}
|
79
src/Symfony/Component/Ldap/Security/LdapAuthenticator.php
Normal file
79
src/Symfony/Component/Ldap/Security/LdapAuthenticator.php
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Ldap\Security;
|
||||||
|
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class decorates internal authenticators to add the LDAP integration.
|
||||||
|
*
|
||||||
|
* In your own authenticators, it is recommended to directly use the
|
||||||
|
* LdapBadge in the authenticate() method. This class should only be
|
||||||
|
* used for Symfony or third party authenticators.
|
||||||
|
*
|
||||||
|
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
* @experimental in Symfony 5.1
|
||||||
|
*/
|
||||||
|
class LdapAuthenticator implements AuthenticatorInterface
|
||||||
|
{
|
||||||
|
private $authenticator;
|
||||||
|
private $ldapServiceId;
|
||||||
|
private $dnString;
|
||||||
|
private $searchDn;
|
||||||
|
private $searchPassword;
|
||||||
|
private $queryString;
|
||||||
|
|
||||||
|
public function __construct(AuthenticatorInterface $authenticator, string $ldapServiceId, string $dnString = '{username}', string $searchDn = '', string $searchPassword = '', string $queryString = '')
|
||||||
|
{
|
||||||
|
$this->authenticator = $authenticator;
|
||||||
|
$this->ldapServiceId = $ldapServiceId;
|
||||||
|
$this->dnString = $dnString;
|
||||||
|
$this->searchDn = $searchDn;
|
||||||
|
$this->searchPassword = $searchPassword;
|
||||||
|
$this->queryString = $queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
return $this->authenticator->supports($request);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(Request $request): PassportInterface
|
||||||
|
{
|
||||||
|
$passport = $this->authenticator->authenticate($request);
|
||||||
|
$passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString));
|
||||||
|
|
||||||
|
return $passport;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||||
|
{
|
||||||
|
return $this->authenticator->createAuthenticatedToken($passport, $firewallName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
return $this->authenticator->onAuthenticationFailure($request, $exception);
|
||||||
|
}
|
||||||
|
}
|
78
src/Symfony/Component/Ldap/Security/LdapBadge.php
Normal file
78
src/Symfony/Component/Ldap/Security/LdapBadge.php
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Ldap\Security;
|
||||||
|
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A badge indicating that the credentials should be checked using LDAP.
|
||||||
|
*
|
||||||
|
* This badge must be used together with PasswordCredentials.
|
||||||
|
*
|
||||||
|
* @author Wouter de Jong <wouter@wouterj.nl>
|
||||||
|
*
|
||||||
|
* @final
|
||||||
|
* @experimental in Symfony 5.1
|
||||||
|
*/
|
||||||
|
class LdapBadge implements BadgeInterface
|
||||||
|
{
|
||||||
|
private $resolved = false;
|
||||||
|
private $ldapServiceId;
|
||||||
|
private $dnString;
|
||||||
|
private $searchDn;
|
||||||
|
private $searchPassword;
|
||||||
|
private $queryString;
|
||||||
|
|
||||||
|
public function __construct(string $ldapServiceId, string $dnString = '{username}', string $searchDn = '', string $searchPassword = '', ?string $queryString = null)
|
||||||
|
{
|
||||||
|
$this->ldapServiceId = $ldapServiceId;
|
||||||
|
$this->dnString = $dnString;
|
||||||
|
$this->searchDn = $searchDn;
|
||||||
|
$this->searchPassword = $searchPassword;
|
||||||
|
$this->queryString = $queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLdapServiceId(): string
|
||||||
|
{
|
||||||
|
return $this->ldapServiceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDnString(): string
|
||||||
|
{
|
||||||
|
return $this->dnString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSearchDn(): string
|
||||||
|
{
|
||||||
|
return $this->searchDn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getSearchPassword(): string
|
||||||
|
{
|
||||||
|
return $this->searchPassword;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getQueryString(): ?string
|
||||||
|
{
|
||||||
|
return $this->queryString;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function markResolved(): void
|
||||||
|
{
|
||||||
|
$this->resolved = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isResolved(): bool
|
||||||
|
{
|
||||||
|
return $this->resolved;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,223 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Symfony package.
|
||||||
|
*
|
||||||
|
* (c) Fabien Potencier <fabien@symfony.com>
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace Symfony\Component\Ldap\Tests\Security;
|
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Psr\Container\ContainerInterface;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\Ldap\Adapter\CollectionInterface;
|
||||||
|
use Symfony\Component\Ldap\Adapter\QueryInterface;
|
||||||
|
use Symfony\Component\Ldap\Entry;
|
||||||
|
use Symfony\Component\Ldap\Exception\ConnectionException;
|
||||||
|
use Symfony\Component\Ldap\LdapInterface;
|
||||||
|
use Symfony\Component\Ldap\Security\CheckLdapCredentialsListener;
|
||||||
|
use Symfony\Component\Ldap\Security\LdapBadge;
|
||||||
|
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||||
|
use Symfony\Component\Security\Core\Exception\AuthenticationException;
|
||||||
|
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
|
||||||
|
use Symfony\Component\Security\Core\User\User;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
|
||||||
|
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;
|
||||||
|
use Symfony\Component\Security\Http\Event\CheckPassportEvent;
|
||||||
|
use Symfony\Contracts\Service\ServiceLocatorTrait;
|
||||||
|
|
||||||
|
class CheckLdapCredentialsListenerTest extends TestCase
|
||||||
|
{
|
||||||
|
private $ldap;
|
||||||
|
|
||||||
|
protected function setUp(): void
|
||||||
|
{
|
||||||
|
if (!interface_exists(AuthenticatorInterface::class)) {
|
||||||
|
$this->markTestSkipped('This test requires symfony/security-http:^5.1');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->ldap = $this->createMock(LdapInterface::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideShouldNotCheckPassport
|
||||||
|
*/
|
||||||
|
public function testShouldNotCheckPassport($authenticator, $passport)
|
||||||
|
{
|
||||||
|
$this->ldap->expects($this->never())->method('bind');
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport(new CheckPassportEvent($authenticator, $passport));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideShouldNotCheckPassport()
|
||||||
|
{
|
||||||
|
if (!interface_exists(AuthenticatorInterface::class)) {
|
||||||
|
$this->markTestSkipped('This test requires symfony/security-http:^5.1');
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = new User('Wouter', null, ['ROLE_USER']);
|
||||||
|
// no LdapBadge
|
||||||
|
yield [new TestAuthenticator(), new Passport($user, new PasswordCredentials('s3cret'))];
|
||||||
|
|
||||||
|
// ldap already resolved
|
||||||
|
$badge = new LdapBadge('app.ldap');
|
||||||
|
$badge->markResolved();
|
||||||
|
yield [new TestAuthenticator(), new Passport($user, new PasswordCredentials('s3cret'), [$badge])];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testPasswordCredentialsAlreadyResolvedThrowsException()
|
||||||
|
{
|
||||||
|
$this->expectException(\LogicException::class);
|
||||||
|
$this->expectExceptionMessage('LDAP authentication password verification cannot be completed because something else has already resolved the PasswordCredentials.');
|
||||||
|
|
||||||
|
$badge = new PasswordCredentials('s3cret');
|
||||||
|
$badge->markResolved();
|
||||||
|
$user = new User('Wouter', null, ['ROLE_USER']);
|
||||||
|
$passport = new Passport($user, $badge, [new LdapBadge('app.ldap')]);
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport(new CheckPassportEvent(new TestAuthenticator(), $passport));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidLdapServiceId()
|
||||||
|
{
|
||||||
|
$this->expectException(\LogicException::class);
|
||||||
|
$this->expectExceptionMessage('Cannot check credentials using the "not_existing_ldap_service" ldap service, as such service is not found. Did you maybe forget to add the "ldap" service tag to this service?');
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('not_existing_ldap_service')));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider provideWrongPassportData
|
||||||
|
*/
|
||||||
|
public function testWrongPassport($passport)
|
||||||
|
{
|
||||||
|
$this->expectException(\LogicException::class);
|
||||||
|
$this->expectExceptionMessage('LDAP authentication requires a passport containing a user and password credentials, authenticator "'.TestAuthenticator::class.'" does not fulfill these requirements.');
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport(new CheckPassportEvent(new TestAuthenticator(), $passport));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function provideWrongPassportData()
|
||||||
|
{
|
||||||
|
if (!interface_exists(AuthenticatorInterface::class)) {
|
||||||
|
$this->markTestSkipped('This test requires symfony/security-http:^5.1');
|
||||||
|
}
|
||||||
|
|
||||||
|
// no password credentials
|
||||||
|
yield [new SelfValidatingPassport(new User('Wouter', null, ['ROLE_USER']), [new LdapBadge('app.ldap')])];
|
||||||
|
|
||||||
|
// no user passport
|
||||||
|
$passport = $this->createMock(PassportInterface::class);
|
||||||
|
$passport->expects($this->any())->method('hasBadge')->with(LdapBadge::class)->willReturn(true);
|
||||||
|
$passport->expects($this->any())->method('getBadge')->with(LdapBadge::class)->willReturn(new LdapBadge('app.ldap'));
|
||||||
|
yield [$passport];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyPasswordShouldThrowAnException()
|
||||||
|
{
|
||||||
|
$this->expectException(BadCredentialsException::class);
|
||||||
|
$this->expectExceptionMessage('The presented password cannot be empty.');
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport($this->createEvent(''));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testBindFailureShouldThrowAnException()
|
||||||
|
{
|
||||||
|
$this->expectException(BadCredentialsException::class);
|
||||||
|
$this->expectExceptionMessage('The presented password is invalid.');
|
||||||
|
|
||||||
|
$this->ldap->expects($this->any())->method('bind')->willThrowException(new ConnectionException());
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport($this->createEvent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testQueryForDn()
|
||||||
|
{
|
||||||
|
$collection = new \ArrayIterator([new Entry('')]);
|
||||||
|
|
||||||
|
$query = $this->getMockBuilder(QueryInterface::class)->getMock();
|
||||||
|
$query->expects($this->once())->method('execute')->willReturn($collection);
|
||||||
|
|
||||||
|
$this->ldap->expects($this->at(0))->method('bind')->with('elsa', 'test1234A$');
|
||||||
|
$this->ldap->expects($this->any())->method('escape')->with('Wouter', '', LdapInterface::ESCAPE_FILTER)->willReturn('wouter');
|
||||||
|
$this->ldap->expects($this->once())->method('query')->with('{username}', 'wouter_test')->willReturn($query);
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{username}', 'elsa', 'test1234A$', '{username}_test')));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testEmptyQueryResultShouldThrowAnException()
|
||||||
|
{
|
||||||
|
$this->expectException(BadCredentialsException::class);
|
||||||
|
$this->expectExceptionMessage('The presented username is invalid.');
|
||||||
|
|
||||||
|
$collection = $this->getMockBuilder(CollectionInterface::class)->getMock();
|
||||||
|
|
||||||
|
$query = $this->getMockBuilder(QueryInterface::class)->getMock();
|
||||||
|
$query->expects($this->once())->method('execute')->willReturn($collection);
|
||||||
|
|
||||||
|
$this->ldap->expects($this->at(0))->method('bind')->with('elsa', 'test1234A$');
|
||||||
|
$this->ldap->expects($this->once())->method('query')->willReturn($query);
|
||||||
|
|
||||||
|
$listener = $this->createListener();
|
||||||
|
$listener->onCheckPassport($this->createEvent('s3cr3t', new LdapBadge('app.ldap', '{username}', 'elsa', 'test1234A$', '{username}_test')));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createEvent($password = 's3cr3t', $ldapBadge = null)
|
||||||
|
{
|
||||||
|
return new CheckPassportEvent(
|
||||||
|
new TestAuthenticator(),
|
||||||
|
new Passport(new User('Wouter', null, ['ROLE_USER']), new PasswordCredentials($password), [$ldapBadge ?? new LdapBadge('app.ldap')])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function createListener()
|
||||||
|
{
|
||||||
|
$ldapLocator = new class(['app.ldap' => function () {
|
||||||
|
return $this->ldap;
|
||||||
|
}]) implements ContainerInterface {
|
||||||
|
use ServiceLocatorTrait;
|
||||||
|
};
|
||||||
|
|
||||||
|
return new CheckLdapCredentialsListener($ldapLocator);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (interface_exists(AuthenticatorInterface::class)) {
|
||||||
|
class TestAuthenticator implements AuthenticatorInterface
|
||||||
|
{
|
||||||
|
public function supports(Request $request): ?bool
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function authenticate(Request $request): PassportInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user