Skip to content

[2.8] [Ldap] Added support for LDAP (New Component + integration in the Security Component). #14602

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 28, 2015
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Implemented LDAP authentication and LDAP user provider
  • Loading branch information
lyrixx authored and csarrazi committed Sep 28, 2015
commit 60b9f2e7ec4538a85eeb4ece7922ebe5531bd60f
Original file line number Diff line number Diff line change
@@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're making it impossible like this to attach a custom authenticator. Can you extend the SimpleFormFactory instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These factories are only to be used with LDAP, and actually override the authentication provider to be the LDAP bind authentication provider. As it already handles authentication, I don't really see why you would want to attach a custom authenticator.

Can you provide a use case?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For example if a user account is disabled, a certain token is not supplied etc. LDAP should be nothing more than a specific user provider in my opinion. Instead of fetching it from the database, you fetch it from ldap.

For me it's currently impossible to do the following:

  • Custom validation on the user account
  • Restrict logins if a user account is in a certain ldap-group
  • Adding a 3rd required field, for example specifying a second "username", where the uniqueness lies in (for example) vendor + username

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This issue should start to be addressed in #14673 (with the new guard authentication system), and maybe #14713.

This feature will be adapted for the new guard system, and improved once these PRs. Depending on when (and whether) these two PRs will be merged, a new PR should be made to update the legacy authentication providers. Until then, I will fix the tests, unless there actually is another issue.

Ping @iltar @weaverryan

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the guard PR gets merged and we can build this on top of that, that's awesome! Realistically, since security is so complex, I imagine that we (the community) will make a push on all these security-related PR's at the same time and decide how it will all end up. For now, I agree with making this PR as solid as possible without #14673 and #14713... though I hope that'll change ;).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. I keep the PR as is. I'll fix the tests, and that should do it.

Of course, I'm open for any suggestion.

Ping @iltar @stof @weaverryan

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think core features in the Security component should still be built with core features. Guard is more for end-users.

{
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';
}
}
Original file line number Diff line number Diff line change
@@ -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';
}
}
Original file line number Diff line number Diff line change
@@ -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()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

 base_dn + search_dn + search_password should be required (and not empty?)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, you can be authenticated anonymously using ldap_bind server.

See http://php.net/manual/en/function.ldap-bind.php:

If bind_rdn and bind_password are not specified, an anonymous bind is attempted.

Regarding the base_dn, it should indeed be required, and non-empty.

->arrayNode('default_roles')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is required, as you are accessing this key in self::create

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed!

->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()
;
}
}
12 changes: 12 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/Resources/config/security.xml
Original file line number Diff line number Diff line change
Expand Up @@ -158,17 +158,29 @@
<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">
<argument type="service" id="router" on-invalid="null" />
<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" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -->
Expand Down
6 changes: 6 additions & 0 deletions src/Symfony/Bundle/SecurityBundle/SecurityBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
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;
Expand All @@ -24,6 +26,7 @@
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.
Expand All @@ -38,7 +41,9 @@ public function build(ContainerBuilder $container)

$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());
Expand All @@ -48,6 +53,7 @@ public function build(ContainerBuilder $container)
$extension->addSecurityListenerFactory(new GuardAuthenticationFactory());

$extension->addUserProviderFactory(new InMemoryFactory());
$extension->addUserProviderFactory(new LdapFactory());
$container->addCompilerPass(new AddSecurityVotersPass());
}
}
Original file line number Diff line number Diff line change
@@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about adding theses lines ?

        if ('NONE_PROVIDED' === $uuid) {
            throw new UsernameNotFoundException('Username can not be null');
        }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not sure about this one. I don't know where we should put this piece of code.
For exemple, if you are using entity user provider, with guid type for the username column, and with postgres, a Postgres sql exception will be thrown. So one will have to implements a custom entity provider to avoid this error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By $uuid, you mean $username?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oups. Yes.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. Rather than using an UsernameNotFoundException, I'll use an InvalidArgumentException. Is that okay with you?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know. This method should throw an UsernameNotFoundException

}

/**
* {@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.');
}
}
}
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy