From 60b9f2e7ec4538a85eeb4ece7922ebe5531bd60f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Fri, 25 Sep 2015 11:30:03 +0200 Subject: [PATCH] Implemented LDAP authentication and LDAP user provider --- .../Security/Factory/FormLoginLdapFactory.php | 57 +++++++++ .../Security/Factory/HttpBasicLdapFactory.php | 67 +++++++++++ .../Security/UserProvider/LdapFactory.php | 64 ++++++++++ .../Resources/config/security.xml | 12 ++ .../Resources/config/security_listeners.xml | 9 ++ .../Bundle/SecurityBundle/SecurityBundle.php | 6 + .../LdapBindAuthenticationProvider.php | 76 ++++++++++++ .../LdapBindAuthenticationProviderTest.php | 61 ++++++++++ .../Core/Tests/User/LdapUserProviderTest.php | 93 +++++++++++++++ .../Security/Core/User/LdapUserProvider.php | 109 ++++++++++++++++++ .../Component/Security/Core/composer.json | 4 +- src/Symfony/Component/Security/composer.json | 6 +- 12 files changed, 561 insertions(+), 3 deletions(-) create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php create mode 100644 src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php create mode 100644 src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php create mode 100644 src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php create mode 100644 src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php create mode 100644 src/Symfony/Component/Security/Core/User/LdapUserProvider.php diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php new file mode 100644 index 0000000000..c758b32b8d --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php @@ -0,0 +1,57 @@ + + * + * 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\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * FormLoginLdapFactory creates services for form login ldap authentication. + * + * @author Grégoire Pineau + * @author Charles Sarrazin + */ +class FormLoginLdapFactory extends FormLoginFactory +{ + protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId) + { + $provider = 'security.authentication.provider.ldap_bind.'.$id; + $container + ->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.ldap_bind')) + ->replaceArgument(0, new Reference($userProviderId)) + ->replaceArgument(2, $id) + ->replaceArgument(3, new Reference($config['service'])) + ->replaceArgument(4, $config['dn_string']) + ; + + return $provider; + } + + public function addConfiguration(NodeDefinition $node) + { + parent::addConfiguration($node); + + $node + ->children() + ->scalarNode('service')->end() + ->scalarNode('dn_string')->defaultValue('{username}')->end() + ->end() + ; + } + + public function getKey() + { + return 'form-login-ldap'; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php new file mode 100644 index 0000000000..23c0130584 --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php @@ -0,0 +1,67 @@ + + * + * 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\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * HttpBasicFactory creates services for HTTP basic authentication. + * + * @author Fabien Potencier + * @author Grégoire Pineau + * @author Charles Sarrazin + */ +class HttpBasicLdapFactory extends HttpBasicFactory +{ + public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint) + { + $provider = 'security.authentication.provider.ldap_bind.'.$id; + $container + ->setDefinition($provider, new DefinitionDecorator('security.authentication.provider.ldap_bind')) + ->replaceArgument(0, new Reference($userProvider)) + ->replaceArgument(2, $id) + ->replaceArgument(3, new Reference($config['service'])) + ->replaceArgument(4, $config['dn_string']) + ; + + // entry point + $entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint); + + // listener + $listenerId = 'security.authentication.listener.basic.'.$id; + $listener = $container->setDefinition($listenerId, new DefinitionDecorator('security.authentication.listener.basic')); + $listener->replaceArgument(2, $id); + $listener->replaceArgument(3, new Reference($entryPointId)); + + return array($provider, $listenerId, $entryPointId); + } + + public function addConfiguration(NodeDefinition $node) + { + parent::addConfiguration($node); + + $node + ->children() + ->scalarNode('service')->end() + ->scalarNode('dn_string')->defaultValue('{username}')->end() + ->end() + ; + } + + public function getKey() + { + return 'http-basic-ldap'; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php new file mode 100644 index 0000000000..068cda6a1f --- /dev/null +++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/UserProvider/LdapFactory.php @@ -0,0 +1,64 @@ + + * + * 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\UserProvider; + +use Symfony\Component\Config\Definition\Builder\NodeDefinition; +use Symfony\Component\DependencyInjection\DefinitionDecorator; +use Symfony\Component\DependencyInjection\ContainerBuilder; +use Symfony\Component\DependencyInjection\Reference; + +/** + * LdapFactory creates services for Ldap user provider. + * + * @author Grégoire Pineau + * @author Charles Sarrazin + */ +class LdapFactory implements UserProviderFactoryInterface +{ + public function create(ContainerBuilder $container, $id, $config) + { + $container + ->setDefinition($id, new DefinitionDecorator('security.user.provider.ldap')) + ->replaceArgument(0, new Reference($config['service'])) + ->replaceArgument(1, $config['base_dn']) + ->replaceArgument(2, $config['search_dn']) + ->replaceArgument(3, $config['search_password']) + ->replaceArgument(4, $config['default_roles']) + ->replaceArgument(5, $config['uid_key']) + ->replaceArgument(6, $config['filter']) + ; + } + + public function getKey() + { + return 'ldap'; + } + + public function addConfiguration(NodeDefinition $node) + { + $node + ->children() + ->scalarNode('service')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('base_dn')->isRequired()->cannotBeEmpty()->end() + ->scalarNode('search_dn')->end() + ->scalarNode('search_password')->end() + ->arrayNode('default_roles') + ->beforeNormalization()->ifString()->then(function($v) { return preg_split('/\s*,\s*/', $v); })->end() + ->requiresAtLeastOneElement() + ->prototype('scalar')->end() + ->end() + ->scalarNode('uid_key')->defaultValue('sAMAccountName')->end() + ->scalarNode('filter')->defaultValue('({uid_key}={username})')->end() + ->end() + ; + } +} diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml index b7c1407c1c..b1a2cdfc80 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml @@ -158,10 +158,21 @@ + + + + + + + + + + + @@ -169,6 +180,7 @@ + diff --git a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml index 917e90f792..948286bb5a 100644 --- a/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml +++ b/src/Symfony/Bundle/SecurityBundle/Resources/config/security_listeners.xml @@ -223,6 +223,15 @@ %security.authentication.hide_user_not_found% + + + + + + + %security.authentication.hide_user_not_found% + + diff --git a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php index f2dcab1061..f2dfc991fb 100644 --- a/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php +++ b/src/Symfony/Bundle/SecurityBundle/SecurityBundle.php @@ -15,7 +15,9 @@ use Symfony\Component\HttpKernel\Bundle\Bundle; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Bundle\SecurityBundle\DependencyInjection\Compiler\AddSecurityVotersPass; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\FormLoginLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpBasicLdapFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\HttpDigestFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\RememberMeFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\X509Factory; @@ -24,6 +26,7 @@ use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimplePre use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\SimpleFormFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\InMemoryFactory; use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\Factory\GuardAuthenticationFactory; +use Symfony\Bundle\SecurityBundle\DependencyInjection\Security\UserProvider\LdapFactory; /** * Bundle. @@ -38,7 +41,9 @@ class SecurityBundle extends Bundle $extension = $container->getExtension('security'); $extension->addSecurityListenerFactory(new FormLoginFactory()); + $extension->addSecurityListenerFactory(new FormLoginLdapFactory()); $extension->addSecurityListenerFactory(new HttpBasicFactory()); + $extension->addSecurityListenerFactory(new HttpBasicLdapFactory()); $extension->addSecurityListenerFactory(new HttpDigestFactory()); $extension->addSecurityListenerFactory(new RememberMeFactory()); $extension->addSecurityListenerFactory(new X509Factory()); @@ -48,6 +53,7 @@ class SecurityBundle extends Bundle $extension->addSecurityListenerFactory(new GuardAuthenticationFactory()); $extension->addUserProviderFactory(new InMemoryFactory()); + $extension->addUserProviderFactory(new LdapFactory()); $container->addCompilerPass(new AddSecurityVotersPass()); } } diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php new file mode 100644 index 0000000000..9ce3bd2428 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php @@ -0,0 +1,76 @@ + + */ +class LdapBindAuthenticationProvider extends UserAuthenticationProvider +{ + private $userProvider; + private $ldap; + private $dnString; + + /** + * Constructor. + * + * @param UserProviderInterface $userProvider A UserProvider + * @param UserCheckerInterface $userChecker A UserChecker + * @param string $providerKey The provider key + * @param LdapClientInterface $ldap An Ldap client + * @param string $dnString A string used to create the bind DN + * @param bool $hideUserNotFoundExceptions Whether to hide user not found exception or not + */ + public function __construct(UserProviderInterface $userProvider, UserCheckerInterface $userChecker, $providerKey, LdapClientInterface $ldap, $dnString = '{username}', $hideUserNotFoundExceptions = true) + { + parent::__construct($userChecker, $providerKey, $hideUserNotFoundExceptions); + + $this->userProvider = $userProvider; + $this->ldap = $ldap; + $this->dnString = $dnString; + } + + /** + * {@inheritdoc} + */ + protected function retrieveUser($username, UsernamePasswordToken $token) + { + if ('NONE_PROVIDED' === $username) { + throw new UsernameNotFoundException('Username can not be null'); + } + + return $this->userProvider->loadUserByUsername($username); + } + + /** + * {@inheritdoc} + */ + protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token) + { + $username = $token->getUsername(); + $password = $token->getCredentials(); + + try { + $username = $this->ldap->escape($username, '', LdapClientInterface::LDAP_ESCAPE_DN); + $dn = str_replace('{username}', $username, $this->dnString); + + $this->ldap->bind($dn, $password); + } catch (ConnectionException $e) { + throw new BadCredentialsException('The presented password is invalid.'); + } + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php new file mode 100644 index 0000000000..f1b5c0355f --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/Authentication/Provider/LdapBindAuthenticationProviderTest.php @@ -0,0 +1,61 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\Tests\Authentication\Provider; + +use Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider; +use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken; +use Symfony\Component\Security\Core\User\User; +use Symfony\Component\Ldap\Exception\ConnectionException; + +class LdapBindAuthenticationProviderTest extends \PHPUnit_Framework_TestCase +{ + /** + * @expectedException \Symfony\Component\Security\Core\Exception\BadCredentialsException + * @expectedExceptionMessage The presented password is invalid. + */ + public function testBindFailureShouldThrowAnException() + { + $userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface'); + $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $ldap + ->expects($this->once()) + ->method('bind') + ->will($this->throwException(new ConnectionException())) + ; + $userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); + + $provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap); + $reflection = new \ReflectionMethod($provider, 'checkAuthentication'); + $reflection->setAccessible(true); + + $reflection->invoke($provider, new User('foo', null), new UsernamePasswordToken('foo', '', 'key')); + } + + public function testRetrieveUser() + { + $userProvider = $this->getMock('Symfony\Component\Security\Core\User\UserProviderInterface'); + $userProvider + ->expects($this->once()) + ->method('loadUserByUsername') + ->with('foo') + ; + $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + + $userChecker = $this->getMock('Symfony\Component\Security\Core\User\UserCheckerInterface'); + + $provider = new LdapBindAuthenticationProvider($userProvider, $userChecker, 'key', $ldap); + $reflection = new \ReflectionMethod($provider, 'retrieveUser'); + $reflection->setAccessible(true); + + $reflection->invoke($provider, 'foo', new UsernamePasswordToken('foo', 'bar', 'key')); + } +} diff --git a/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php b/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php new file mode 100644 index 0000000000..b69987aa53 --- /dev/null +++ b/src/Symfony/Component/Security/Core/Tests/User/LdapUserProviderTest.php @@ -0,0 +1,93 @@ +getMock('Symfony\Component\Ldap\LdapClientInterface'); + $ldap + ->expects($this->once()) + ->method('bind') + ->will($this->throwException(new ConnectionException())) + ; + + $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com'); + $provider->loadUserByUsername('foo'); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException + */ + public function testLoadUserByUsernameFailsIfNoLdapEntries() + { + $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $ldap + ->expects($this->once()) + ->method('escape') + ->will($this->returnValue('foo')) + ; + + $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com'); + $provider->loadUserByUsername('foo'); + } + + /** + * @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException + */ + public function testLoadUserByUsernameFailsIfMoreThanOneLdapEntry() + { + $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $ldap + ->expects($this->once()) + ->method('escape') + ->will($this->returnValue('foo')) + ; + $ldap + ->expects($this->once()) + ->method('find') + ->will($this->returnValue(array( + array(), + array(), + 'count' => 2, + ))) + ; + + $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com'); + $provider->loadUserByUsername('foo'); + } + + public function testSuccessfulLoadUserByUsername() + { + $ldap = $this->getMock('Symfony\Component\Ldap\LdapClientInterface'); + $ldap + ->expects($this->once()) + ->method('escape') + ->will($this->returnValue('foo')) + ; + $ldap + ->expects($this->once()) + ->method('find') + ->will($this->returnValue(array( + array( + 'sAMAccountName' => 'foo', + 'userpassword' => 'bar', + ), + 'count' => 1, + ))) + ; + + $provider = new LdapUserProvider($ldap, 'ou=MyBusiness,dc=symfony,dc=com'); + $this->assertInstanceOf( + 'Symfony\Component\Security\Core\User\User', + $provider->loadUserByUsername('foo') + ); + } +} diff --git a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php new file mode 100644 index 0000000000..ec699fc022 --- /dev/null +++ b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Security\Core\User; + +use Symfony\Component\Security\Core\Exception\UnsupportedUserException; +use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; +use Symfony\Component\Ldap\Exception\ConnectionException; +use Symfony\Component\Ldap\LdapClientInterface; + +/** + * LdapUserProvider is a simple user provider on top of ldap. + * + * @author Grégoire Pineau + * @author Charles Sarrazin + */ +class LdapUserProvider implements UserProviderInterface +{ + private $ldap; + private $baseDn; + private $searchDn; + private $searchPassword; + private $defaultRoles; + private $defaultSearch; + + /** + * @param LdapClientInterface $ldap + * @param string $baseDn + * @param string $searchDn + * @param string $searchPassword + * @param array $defaultRoles + * @param string $uidKey + * @param string $filter + */ + public function __construct(LdapClientInterface $ldap, $baseDn, $searchDn = null, $searchPassword = null, array $defaultRoles = array(), $uidKey = 'sAMAccountName', $filter = '({uid_key}={username})') + { + $this->ldap = $ldap; + $this->baseDn = $baseDn; + $this->searchDn = $searchDn; + $this->searchPassword = $searchPassword; + $this->defaultRoles = $defaultRoles; + $this->defaultSearch = str_replace('{uid_key}', $uidKey, $filter); + } + + /** + * {@inheritdoc} + */ + public function loadUserByUsername($username) + { + try { + $this->ldap->bind($this->searchDn, $this->searchPassword); + $username = $this->ldap->escape($username, '', LdapClientInterface::LDAP_ESCAPE_FILTER); + $query = str_replace('{username}', $username, $this->defaultSearch); + $search = $this->ldap->find($this->baseDn, $query); + } catch (ConnectionException $e) { + throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username), 0, $e); + } + + if (!$search) { + throw new UsernameNotFoundException(sprintf('User "%s" not found.', $username)); + } + + if ($search['count'] > 1) { + throw new UsernameNotFoundException('More than one user found'); + } + + $user = $search[0]; + + return $this->loadUser($username, $user); + } + + public function loadUser($username, $user) + { + $password = isset($user['userpassword']) ? $user['userpassword'] : null; + + $roles = $this->defaultRoles; + + return new User($username, $password, $roles); + } + + /** + * {@inheritdoc} + */ + public function refreshUser(UserInterface $user) + { + if (!$user instanceof User) { + throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', get_class($user))); + } + + return new User($user->getUsername(), null, $user->getRoles()); + } + + /** + * {@inheritdoc} + */ + public function supportsClass($class) + { + return $class === 'Symfony\Component\Security\Core\User\User'; + } + +} diff --git a/src/Symfony/Component/Security/Core/composer.json b/src/Symfony/Component/Security/Core/composer.json index 4d2405317c..6a1ac993c9 100644 --- a/src/Symfony/Component/Security/Core/composer.json +++ b/src/Symfony/Component/Security/Core/composer.json @@ -26,13 +26,15 @@ "symfony/translation": "~2.0,>=2.0.5|~3.0.0", "symfony/validator": "~2.5,>=2.5.5|~3.0.0", "psr/log": "~1.0", - "ircmaxell/password-compat": "1.0.*" + "ircmaxell/password-compat": "1.0.*", + "symfony/ldap": "~2.8|~3.0.0" }, "suggest": { "symfony/event-dispatcher": "", "symfony/http-foundation": "", "symfony/validator": "For using the user password constraint", "symfony/expression-language": "For using the expression voter", + "symfony/ldap": "For using LDAP integration", "ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5" }, "autoload": { diff --git a/src/Symfony/Component/Security/composer.json b/src/Symfony/Component/Security/composer.json index e4e9a8b701..87a47a53ad 100644 --- a/src/Symfony/Component/Security/composer.json +++ b/src/Symfony/Component/Security/composer.json @@ -38,7 +38,8 @@ "doctrine/dbal": "~2.2", "psr/log": "~1.0", "ircmaxell/password-compat": "~1.0", - "symfony/expression-language": "~2.6|~3.0.0" + "symfony/expression-language": "~2.6|~3.0.0", + "symfony/ldap": "~2.8|~3.0.0" }, "suggest": { "symfony/class-loader": "For using the ACL generateSql script", @@ -48,7 +49,8 @@ "symfony/routing": "For using the HttpUtils class to create sub-requests, redirect the user, and match URLs", "symfony/expression-language": "For using the expression voter", "ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5", - "paragonie/random_compat": "" + "paragonie/random_compat": "", + "symfony/ldap": "For using the LDAP user and authentication providers" }, "autoload": { "psr-4": { "Symfony\\Component\\Security\\": "" }