Implemented LDAP authentication and LDAP user provider

This commit is contained in:
Grégoire Pineau 2015-09-25 11:30:03 +02:00 committed by Charles Sarrazin
parent 1c964b993f
commit 60b9f2e7ec
12 changed files with 561 additions and 3 deletions

View File

@ -0,0 +1,57 @@
<?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\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 <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
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';
}
}

View File

@ -0,0 +1,67 @@
<?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\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 <fabien@symfony.com>
* @author Grégoire Pineau <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
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';
}
}

View File

@ -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\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 <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
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()
;
}
}

View File

@ -158,10 +158,21 @@
<argument type="service" id="security.token_storage" on-invalid="null" />
</service>
<!-- Provisioning -->
<service id="security.user.provider.in_memory" class="%security.user.provider.in_memory.class%" abstract="true" public="false" />
<service id="security.user.provider.in_memory.user" class="%security.user.provider.in_memory.user.class%" abstract="true" public="false" />
<service id="security.user.provider.ldap" class="Symfony\Component\Security\Core\User\LdapUserProvider" abstract="true" public="false">
<argument /> <!-- security.ldap.ldap -->
<argument /> <!-- base dn -->
<argument /> <!-- search dn -->
<argument /> <!-- search password -->
<argument /> <!-- default_roles -->
<argument /> <!-- uid key -->
<argument /> <!-- filter -->
</service>
<service id="security.user.provider.chain" class="%security.user.provider.chain.class%" abstract="true" public="false" />
<service id="security.http_utils" class="%security.http_utils.class%" public="false">
@ -169,6 +180,7 @@
<argument type="service" id="router" on-invalid="null" />
</service>
<!-- Validator -->
<service id="security.validator.user_password" class="%security.validator.user_password.class%">
<tag name="validator.constraint_validator" alias="security.validator.user_password" />

View File

@ -223,6 +223,15 @@
<argument>%security.authentication.hide_user_not_found%</argument>
</service>
<service id="security.authentication.provider.ldap_bind" class="Symfony\Component\Security\Core\Authentication\Provider\LdapBindAuthenticationProvider" public="false" abstract="true">
<argument /> <!-- User Provider -->
<argument type="service" id="security.user_checker" />
<argument /> <!-- Provider-shared Key -->
<argument /> <!-- LDAP -->
<argument /> <!-- Base DN -->
<argument>%security.authentication.hide_user_not_found%</argument>
</service>
<service id="security.authentication.provider.simple" class="%security.authentication.provider.simple.class%" abstract="true" public="false">
<argument /> <!-- Simple Authenticator -->
<argument /> <!-- User Provider -->

View File

@ -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());
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace Symfony\Component\Security\Core\Authentication\Provider;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserCheckerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Ldap\LdapClientInterface;
use Symfony\Component\Ldap\Exception\ConnectionException;
/**
* LdapBindAuthenticationProvider authenticates a user against an LDAP server.
*
* The only way to check user credentials is to try to connect the user with its
* credentials to the ldap.
*
* @author Charles Sarrazin <charles@sarraz.in>
*/
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.');
}
}
}

View File

@ -0,0 +1,61 @@
<?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\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'));
}
}

View File

@ -0,0 +1,93 @@
<?php
namespace Symfony\Component\Security\Core\Tests\User;
use Symfony\Component\Security\Core\User\LdapUserProvider;
use Symfony\Component\Ldap\Exception\ConnectionException;
class LdapUserProviderTest extends \PHPUnit_Framework_TestCase
{
/**
* @expectedException \Symfony\Component\Security\Core\Exception\UsernameNotFoundException
*/
public function testLoadUserByUsernameFailsIfCantConnectToLdap()
{
$ldap = $this->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')
);
}
}

View File

@ -0,0 +1,109 @@
<?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\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 <lyrixx@lyrixx.info>
* @author Charles Sarrazin <charles@sarraz.in>
*/
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';
}
}

View File

@ -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": {

View File

@ -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\\": "" }