8000 [Security] Added LDAP support to Authenticator system · symfony/symfony@e71a5a3 · GitHub
[go: up one dir, main page]

Skip to content

Commit e71a5a3

Browse files
committed
[Security] Added LDAP support to Authenticator system
1 parent c30d6f9 commit e71a5a3

File tree

8 files changed

+525
-0
lines changed

8 files changed

+525
-0
lines changed

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@
2727
*/
2828
class FormLoginLdapFactory extends FormLoginFactory
2929
{
30+
use LdapFactoryTrait;
31+
3032
protected function createAuthProvider(ContainerBuilder $container, string $id, array $config, string $userProviderId)
3133
{
3234
$provider = 'security.authentication.provider.ldap_bind.'.$id;

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
*/
2929
class HttpBasicLdapFactory extends HttpBasicFactory
3030
{
31+
use LdapFactoryTrait;
32+
3133
public function create(ContainerBuilder $container, string $id, array $config, string $userProvider, ?string $defaultEntryPoint)
3234
{
3335
$provider = 'security.authentication.provider.ldap_bind.'.$id;

src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/JsonLoginLdapFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
*/
2525
class JsonLoginLdapFactory extends JsonLoginFactory
2626
{
27+
use LdapFactoryTrait;
28+
2729
public function getKey()
2830
{
2931
return 'json-login-ldap';
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory;
13+
14+
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
15+
use Symfony\Component\DependencyInjection\Argument\ServiceLocatorArgument;
16+
use Symfony\Component\DependencyInjection\Argument\TaggedIteratorArgument;
17+
use Symfony\Component\DependencyInjection\ContainerBuilder;
18+
use Symfony\Component\DependencyInjection\Definition;
19+
use Symfony\Component\DependencyInjection\Reference;
20+
use Symfony\Component\Ldap\Security\LdapAuthenticator;
21+
use Symfony\Component\Ldap\Security\VerifyLdapCredentialsListener;
22+
23+
/**
24+
* A trait decorating the authenticator with LDAP functionality.
25+
*
26+
* @author Wouter de Jong <wouter@wouterj.nl>
27+
*
28+
* @internal
29+
*/
30+
trait LdapFactoryTrait
31+
{
32+
public function createAuthenticator(ContainerBuilder $container, string $firewallName, array $config, string $userProviderId): string
33+
{
34+
$key = str_replace('-', '_', $this->getKey());
35+
if (!class_exists(LdapAuthenticator::class)) {
36+
throw new \LogicException(sprintf('The "%s" authenticator requires the "symfony/ldap" package version "5.1" or higher.', $key));
37+
}
38+
39+
$authenticatorId = parent::createAuthenticator($container, $firewallName, $config, $userProviderId);
40+
41+
$container->setDefinition('security.listener.'.$key.'.'.$firewallName, new Definition(VerifyLdapCredentialsListener::class))
42+
->addTag('kernel.event_subscriber', ['dispatcher' => 'security.event_dispatcher.'.$firewallName])
43+
->addArgument(new ServiceLocatorArgument(new TaggedIteratorArgument('ldap')))
44+
;
45+
46+
$ldapAuthenticatorId = 'security.authenticator.'.$key.'.'.$firewallName;
47+
$definition = $container->setDefinition($ldapAuthenticatorId, new Definition(LdapAuthenticator::class))
48+
->setArguments([
49+
new Reference($authenticatorId),
50+
$config['service'],
51+
$config['dn_string'],
52+
$config['search_dn'],
53+
$config['search_password'],
54+
]);
55+
56+
if (!empty($config['query_string'])) {
57+
if ('' === $config['search_dn'] || '' === $config['search_password']) {
58+
throw new InvalidConfigurationException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.');
59+
}
60+
61+
$definition->addArgument($config['query_string']);
62+
}
63+
64+
return $ldapAuthenticatorId;
65+
}
66+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Ldap\Security;
13+
14+
use Symfony\Component\HttpFoundation\Request;
15+
use Symfony\Component\HttpFoundation\Response;
16+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
17+
use Symfony\Component\Security\Core\Exception\AuthenticationException;
18+
use Symfony\Component\Security\Http\Authenticator\AuthenticatorInterface;
19+
use Symfony\Component\Security\Http\Authenticator\Passport\PassportInterface;
20+
21+
/**
22+
* @author Wouter de Jong <wouter@wouterj.nl>
23+
*
24+
* @final
25+
* @experimental in Symfony 5.1
26+
*/
27+
class LdapAuthenticator implements AuthenticatorInterface
28+
{
29+
private $authenticator;
30+
private $ldapServiceId;
31+
private $dnString;
32+
private $searchDn;
33+
private $searchPassword;
34+
private $queryString;
35+
36+
public function __construct(AuthenticatorInterface $authenticator, string $ldapServiceId, string $dnString = '{username}', string $searchDn = '', string $searchPassword = '', string $queryString = '')
37+
{
38+
$this->authenticator = $authenticator;
39+
$this->ldapServiceId = $ldapServiceId;
40+
$this->dnString = $dnString;
41+
$this->searchDn = $searchDn;
42+
$this->searchPassword = $searchPassword;
43+
$this->queryString = $queryString;
44+
}
45+
46+
public function supports(Request $request): ?bool
47+
{
48+
return $this->authenticator->supports($request);
49+
}
50+
51+
public function authenticate(Request $request): PassportInterface
52+
{
53+
$passport = $this->authenticator->authenticate($request);
54+
$passport->addBadge(new LdapBadge($this->ldapServiceId, $this->dnString, $this->searchDn, $this->searchPassword, $this->queryString));
55+
56+
return $passport;
57+
}
58+
59+
public function createAuthenticatedToken(PassportInterface $passport, string $firewallName): TokenInterface
60+
{
61+
return $this->authenticator->createAuthenticatedToken($passport, $firewallName);
62+
}
63+
64+
public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
65+
{
66+
return $this->authenticator->onAuthenticationSuccess($request, $token, $firewallName);
67+
}
68+
69+
public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
70+
{
71+
return $this->authenticator->onAuthenticationFailure($request, $exception);
72+
}
73+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Ldap\Security;
13+
14+
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\BadgeInterface;
15+
16+
/**
17+
* @author Wouter de Jong <wouter@wouterj.nl>
18+
*
19+
* @final
20+
* @experimental in Symfony 5.1
21+
*/
22+
class LdapBadge implements BadgeInterface
23+
{
24+
private $resolved = false;
25+
private $ldapServiceId;
26+
private $dnString;
27+
private $searchDn;
28+
private $searchPassword;
29+
private $queryString;
30+
31+
public function __construct(string $ldapServiceId, string $dnString = '{username}', string $searchDn = '', string $searchPassword = '', ?string $queryString = null)
32+
{
33+
$this->ldapServiceId = $ldapServiceId;
34+
$this->dnString = $dnString;
35+
$this->searchDn = $searchDn;
36+
$this->searchPassword = $searchPassword;
37+
$this->queryString = $queryString;
38+
}
39+
40+
public function getLdapServiceId(): string
41+
{
42+
return $this->ldapServiceId;
43+
}
44+
45+
public function getDnString(): string
46+
{
47+
return $this->dnString;
48+
}
49+
50+
public function getSearchDn(): string
51+
{
52+
return $this->searchDn;
53+
}
54+
55+
public function getSearchPassword(): string
56+
{
57+
return $this->searchPassword;
58+
}
59+
60+
public function getQueryString(): ?string
61+
{
62+
return $this->queryString;
63+
}
64+
65+
public function markResolved(): void
66+
{
67+
$this->resolved = true;
68+
}
69+
70+
public function isResolved(): bool
71+
{
72+
return $this->resolved;
73+
}
74+
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <fabien@symfony.com>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Ldap\Security;
13+
14+
use Psr\Container\ContainerInterface;
15+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
16+
use Symfony\Component\Ldap\Exception\ConnectionException;
17+
use Symfony\Component\Ldap\LdapInterface;
18+
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
19+
use Symfony\Component\Security\Core\Exception\LogicException;
20+
use Symfony\Component\Security\Http\Authenticator\Passport\Credentials\PasswordCredentials;
21+
use Symfony\Component\Security\Http\Authenticator\Passport\UserPassportInterface;
22+
use Symfony\Component\Security\Http\Event\VerifyAuthenticatorCredentialsEvent;
23+
24+
/**
25+
* @author Wouter de Jong <wouter@wouterj.nl>
26+
*/
27+
class VerifyLdapCredentialsListener implements EventSubscriberInterface
28+
{
29+
private $ldapLocator;
30+
31+
public function __construct(ContainerInterface $ldapLocator)
32+
{
33+
$this->ldapLocator = $ldapLocator;
34+
}
35+
36+
public function onVerifyCredentials(VerifyAuthenticatorCredentialsEvent $event)
37+
{
38+
$passport = $event->getPassport();
39+
if (!$passport->hasBadge(LdapBadge::class)) {
40+
return;
41+
}
42+
43+
if (!$passport instanceof UserPassportInterface || !$passport->hasBadge(PasswordCredentials::class)) {
44+
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())));
45+
}
46+
47+
/** @var LdapBadge $ldapBadge */
48+
$ldapBadge = $passport->getBadge(LdapBadge::class);
49+
if ($ldapBadge->isResolved()) {
50+
return;
51+
}
52+
53+
/** @var PasswordCredentials $passwordCredentials */
54+
$passwordCredentials = $passport->getBadge(PasswordCredentials::class);
55+
if ($passwordCredentials->isResolved()) {
56+
return;
57+
}
58+
59+
if (!$this->ldapLocator->has($ldapBadge->getLdapServiceId())) {
60+
throw new \LogicException(sprintf('Cannot verify 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()));
61+
}
62+
63+
$presentedPassword = $passwordCredentials->getPassword();
64+
if ('' === $presentedPassword) {
65+
throw new BadCredentialsException('The presented password cannot be empty.');
66+
}
67+
68+
/** @var LdapInterface $ldap */
69+
$ldap = $this->ldapLocator->get($ldapBadge->getLdapServiceId());
70+
try {
71+
if ($ldapBadge->getQueryString()) {
72+
if ('' !== $ldapBadge->getSearchDn() && '' !== $ldapBadge->getSearchPassword()) {
73+
$ldap->bind($ldapBadge->getSearchDn(), $ldapBadge->getSearchPassword());
74+
} else {
75+
throw new LogicException('Using the "query_string" config without using a "search_dn" and a "search_password" is not supported.');
76+
}
77+
$username = $ldap->escape($passport->getUser()->getUsername(), '', LdapInterface::ESCAPE_FILTER);
78+
$query = str_replace('{username}', $username, $ldapBadge->getQueryString());
79+
$result = $ldap->query($ldapBadge->getDnString(), $query)->execute();
80+
if (1 !== $result->count()) {
81+
throw new BadCredentialsException('The presented username is invalid.');
82+
}
83+
84+
$dn = $result[0]->getDn();
85+
} else {
86+
$username = $ldap->escape($passport->getUser()->getUsername(), '', LdapInterface::ESCAPE_DN);
87+
$dn = str_replace('{username}', $username, $ldapBadge->getDnString());
88+
}
89+
90+
$ldap->bind($dn, $presentedPassword);
91+
} catch (ConnectionException $e) {
92+
throw new BadCredentialsException('The presented password is invalid.');
93+
}
94+
95+
$passwordCredentials->markResolved();
96+
$ldapBadge->markResolved();
97+
}
98+
99+
public static function getSubscribedEvents(): array
100+
{
101+
return [VerifyAuthenticatorCredentialsEvent::class => ['onVerifyCredentials', 144]];
102+
}
103+
}

0 commit comments

Comments
 (0)
0