Skip to content

[Security] make LdapBindAuthenticationProvider capable of searching for the DN #21402

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 3 commits into from
Jan 28, 2017

Conversation

lsmith77
Copy link
Contributor

@lsmith77 lsmith77 commented Jan 25, 2017

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets #16823, #20905
License MIT
Doc PR symfony/symfony-docs#7420

I guess due to the separation between the user and auth provider something like the following isn't ok (note: the following works just fine for me but if course in the end the username is the DN and not the user provided shorter username):

diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
index 5ebb09a..18d7825 100644
--- a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
+++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
@@ -70,7 +70,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
      */
     protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
     {
-        $username = $token->getUsername();
+        $username = $user->getUsername();
         $password = $token->getCredentials();
 
         if ('' === $password) {
@@ -78,10 +78,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
         }
 
         try {
-            $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
-            $dn = str_replace('{username}', $username, $this->dnString);
-
-            $this->ldap->bind($dn, $password);
+            $this->ldap->bind($username, $password);
         } catch (ConnectionException $e) {
             throw new BadCredentialsException('The presented password is invalid.');
         }
diff --git a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
index fc42419..8194c4c 100644
--- a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
+++ b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
@@ -115,7 +115,7 @@ class LdapUserProvider implements UserProviderInterface
     {
         $password = $this->getPassword($entry);
 
-        return new User($username, $password, $this->defaultRoles);
+        return new User($entry->getDn(), $password, $this->defaultRoles);
     }
 
     /**

Therefore I created an entire new auth provider.


$provider = 'security.authentication.provider.ldap_'.$method.'.'.$id;

switch($method) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is probably a more elegant way to do this without duplicating so much code

@lsmith77 lsmith77 changed the title added LdapQueryAuthenticationProvider [Security] added LdapQueryAuthenticationProvider Jan 25, 2017
@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch 2 times, most recently from d7b1939 to 12e9d07 Compare January 25, 2017 12:41
@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch from 12e9d07 to a26e0b1 Compare January 25, 2017 12:42
throw new BadCredentialsException('The presented password is invalid.');
}

$this->ldap->bind($result->getIterator()->current()->getDn(), $password);
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 could probably be rewritten as $this->ldap->bind($result[0]->getDn(), $password);


$provider = 'security.authentication.provider.ldap_'.$method.'.'.$id;

switch ($method) {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

there is probably a more elegant way to reduce the code duplication

@nicolas-grekas nicolas-grekas added this to the 3.x milestone Jan 25, 2017
@nicolas-grekas
Copy link
Member

ping @csarrazi

@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch 2 times, most recently from 90395dd to 04b82a8 Compare January 25, 2017 17:44
*
* The only way to check user credentials is to first find the DN via query and then bind
*
* @author Charles Sarrazin <charles@sarraz.in>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@csarrazi since I used LdapBindAuthenticationProvider as the base I figured I should leave you as an author

Copy link
Contributor Author

Choose a reason for hiding this comment

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

BTW I did however think its not appropriate to extend from LdapBindAuthenticationProvider even though it would reduce some code duplication.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

another approach could be to integrate the logic into LdapBindAuthenticationProvider

Copy link
Contributor

Choose a reason for hiding this comment

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

I think it would actually be better to integrate the logic inside LdapBindAuthenticationProvider.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@csarrazi ok .. how should I handle the new parameter without breaking BC on the constructor signature? use setter injection?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

something like

diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php
index cd4158d..3d9d4b2 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/FormLoginLdapFactory.php
@@ -27,7 +27,7 @@ class FormLoginLdapFactory extends FormLoginFactory
     protected function createAuthProvider(ContainerBuilder $container, $id, $config, $userProviderId)
     {
         $provider = 'security.authentication.provider.ldap_bind.'.$id;
-        $container
+        $definition = $container
             ->setDefinition($provider, new ChildDefinition('security.authentication.provider.ldap_bind'))
             ->replaceArgument(0, new Reference($userProviderId))
             ->replaceArgument(1, new Reference('security.user_checker.'.$id))
@@ -36,6 +36,10 @@ class FormLoginLdapFactory extends FormLoginFactory
             ->replaceArgument(4, $config['dn_string'])
         ;
 
+        if (!empty($config['query_string'])) {
+            $definition->addMethodCall('setQueryString', [$config['query_string']]);
+        }
+
         return $provider;
     }
 
@@ -47,6 +51,7 @@ class FormLoginLdapFactory extends FormLoginFactory
             ->children()
                 ->scalarNode('service')->defaultValue('ldap')->end()
                 ->scalarNode('dn_string')->defaultValue('{username}')->end()
+                ->scalarNode('query_string')->end()
             ->end()
         ;
     }
diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
index 5ebb09a..3dfe028 100644
--- a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
+++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
@@ -33,6 +33,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
     private $userProvider;
     private $ldap;
     private $dnString;
+    private $queryString;
 
     /**
      * Constructor.
@@ -54,6 +55,16 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
     }
 
     /**
+     * Set a query string to use in order to find a DN for the username
+     *
+     * @param string $queryString
+     */
+    public function setQueryString($queryString)
+    {
+        $this->queryString = $queryString;
+    }
+
+    /**
      * {@inheritdoc}
      */
     protected function retrieveUser($username, UsernamePasswordToken $token)
@@ -79,7 +90,21 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
 
         try {
             $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
-            $dn = str_replace('{username}', $username, $this->dnString);
+
+            if ($this->queryString) {
+                $query = str_replace('{username}', $username, $this->queryString);
+
+                $query = $this->ldap->query($this->dnString, $query);
+                $result = $query->execute();
+                if (1 !== $result->count()) {
+                    throw new BadCredentialsException('The presented username is invalid.');
+                }
+
+                $dn = $result[0]->getDn();
+            } else {
+                $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
+                $dn = str_replace('{username}', $username, $this->dnString);
+            }
 
             $this->ldap->bind($dn, $password);
         } catch (ConnectionException $e) {

Copy link
Contributor

Choose a reason for hiding this comment

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

Yup, setter injection would be the way to go. By default the new attribute should be null, and the current behaviour should be the correct one in this case.

Copy link
Contributor

Choose a reason for hiding this comment

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

One thing, though. It seems that you have duplicate logic in the else part of your if statement:

$username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);

@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch from 04b82a8 to 4f7487c Compare January 25, 2017 19:42
@lsmith77 lsmith77 changed the title [Security] added LdapQueryAuthenticationProvider [Security] make LdapBindAuthenticationProvider capable of searching for the query string Jan 25, 2017
@lsmith77
Copy link
Contributor Author

@csarrazi pushed (and fixed the duplicate code)

@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch from 4f7487c to a70f80a Compare January 25, 2017 19:44
@lsmith77 lsmith77 changed the title [Security] make LdapBindAuthenticationProvider capable of searching for the query string [Security] make LdapBindAuthenticationProvider capable of searching for the DN Jan 25, 2017
@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch from a70f80a to 93a02b2 Compare January 25, 2017 20:10
@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch from 93a02b2 to a30191f Compare January 25, 2017 20:32
@lsmith77
Copy link
Contributor Author

added some tests

@lsmith77
Copy link
Contributor Author

I will test it tomorrow again when I have direct access to the LDAP server of the project where I am implementing this to confirm the tests are sufficient and the code is indeed working.

@nietonfir
Copy link

nietonfir commented Jan 25, 2017

Just tested (both the original PR and the updated one - against current stable@3.2.2) against an OpenLDAP directory configured with a meta backend serving as a proxy for two different OpenLDAP instances (thus two different DNs - see #16823), works like a charm.

Your changes can also be applied directly to HttpBasicLdapFactory:

diff --git a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php
index 961af71..4665c07 100644
--- a/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php
+++ b/src/Symfony/Bundle/SecurityBundle/DependencyInjection/Security/Factory/HttpBasicLdapFactory.php
@@ -28,7 +28,7 @@ class HttpBasicLdapFactory extends HttpBasicFactory
     public function create(ContainerBuilder $container, $id, $config, $userProvider, $defaultEntryPoint)
     {
         $provider = 'security.authentication.provider.ldap_bind.'.$id;
-        $container
+        $definition = $container
             ->setDefinition($provider, new ChildDefinition('security.authentication.provider.ldap_bind'))
             ->replaceArgument(0, new Reference($userProvider))
             ->replaceArgument(1, new Reference('security.user_checker.'.$id))
@@ -40,6 +40,11 @@ class HttpBasicLdapFactory extends HttpBasicFactory
         // entry point
         $entryPointId = $this->createEntryPoint($container, $id, $config, $defaultEntryPoint);
 
+
+        if (!empty($config['query_string'])) {
+            $definition->addMethodCall('setQueryString', array($config['query_string']));
+        }
+
         // listener
         $listenerId = 'security.authentication.listener.basic.'.$id;
         $listener = $container->setDefinition($listenerId, new ChildDefinition('security.authentication.listener.basic'));
@@ -57,6 +62,7 @@ class HttpBasicLdapFactory extends HttpBasicFactory
             ->children()
                 ->scalarNode('service')->defaultValue('ldap')->end()
                 ->scalarNode('dn_string')->defaultValue('{username}')->end()
+                ->scalarNode('query_string')->end()
             ->end()
         ;
     }

nietonfir pushed a commit to nietonfir/symfony-docs that referenced this pull request Jan 26, 2017
so that it matches the current FormLoginLdapFactory ldap implementation,
where the DN can be search for before binding.
@nietonfir
Copy link

@lsmith77 created a PR to your branch. Hope that's what you meant… ;-)

@lsmith77
Copy link
Contributor Author

thx

nietonfir pushed a commit to nietonfir/symfony that referenced this pull request Jan 26, 2017
@nietonfir nietonfir mentioned this pull request Jan 26, 2017
@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch from 5f89a03 to 0679a74 Compare January 26, 2017 21:47
@lsmith77
Copy link
Contributor Author

added support for HTTP basic as well and linked a doc PR.

will do final testing on my end tomorrow.

@csarrazi are the unit tests sufficient or do we also need some functional tests?


if ($this->queryString) {
$query = str_replace('{username}', $username, $this->queryString);

Copy link
Contributor

Choose a reason for hiding this comment

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

Remove newline.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

$query = str_replace('{username}', $username, $this->queryString);

$query = $this->ldap->query($this->dnString, $query);
$result = $query->execute();
Copy link
Contributor

Choose a reason for hiding this comment

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

Add newline after this one and merge this line with the previous one (no need to keep the query variable, as it is not used for anything but for running its execute() method.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done

@csarrazi
Copy link
Contributor

I don't think we need functional tests for this one. Functional tests are limited to the Ldap component's integration with OpenLDAP. The component was not updated here, so no functional tests are needed. Unit tests are sufficient here.

@lsmith77 lsmith77 force-pushed the ldap_query_auth_provider branch from 0679a74 to 8ddd533 Compare January 27, 2017 07:30
@lsmith77
Copy link
Contributor Author

@csarrazi ok fixed your comments and tested it against the production LDAP server. all is well to merge from my side

@csarrazi
Copy link
Contributor

👍

@fabpot
Copy link
Member

fabpot commented Jan 28, 2017

Thank you @lsmith77.

@fabpot fabpot closed this Jan 28, 2017
@fabpot fabpot merged commit 8ddd533 into symfony:master Jan 28, 2017
fabpot added a commit that referenced this pull request Jan 28, 2017
… of searching for the DN (lsmith77, nietonfir)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Security] make LdapBindAuthenticationProvider capable of searching for the DN

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #16823, #20905
| License       | MIT
| Doc PR        | symfony/symfony-docs#7420

I guess due to the separation between the user and auth provider something like the following isn't ok  (note: the following works just fine for me but if course in the end the username is the DN and not the user provided shorter username):

```diff
diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
index 5ebb09a..18d7825 100644
--- a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
+++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
@@ -70,7 +70,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
      */
     protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
     {
-        $username = $token->getUsername();
+        $username = $user->getUsername();
         $password = $token->getCredentials();

         if ('' === $password) {
@@ -78,10 +78,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
         }

         try {
-            $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
-            $dn = str_replace('{username}', $username, $this->dnString);
-
-            $this->ldap->bind($dn, $password);
+            $this->ldap->bind($username, $password);
         } catch (ConnectionException $e) {
             throw new BadCredentialsException('The presented password is invalid.');
         }
diff --git a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
index fc42419..8194c4c 100644
--- a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
+++ b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
@@ -115,7 +115,7 @@ class LdapUserProvider implements UserProviderInterface
     {
         $password = $this->getPassword($entry);

-        return new User($username, $password, $this->defaultRoles);
+        return new User($entry->getDn(), $password, $this->defaultRoles);
     }

     /**
```

Therefore I created an entire new auth provider.

Commits
-------

8ddd533 Merge pull request #1 from nietonfir/http_basic_ldap
a783e5c Update HttpBasicLdapFactory
a30191f make LdapBindAuthenticationProvider capable of searching for the DN
@lsmith77 lsmith77 deleted the ldap_query_auth_provider branch January 28, 2017 22:49
chalasr pushed a commit to chalasr/symfony that referenced this pull request Jan 29, 2017
…capable of searching for the DN (lsmith77, nietonfir)

This PR was merged into the 3.3-dev branch.

Discussion
----------

[Security] make LdapBindAuthenticationProvider capable of searching for the DN

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | symfony#16823, symfony#20905
| License       | MIT
| Doc PR        | symfony/symfony-docs#7420

I guess due to the separation between the user and auth provider something like the following isn't ok  (note: the following works just fine for me but if course in the end the username is the DN and not the user provided shorter username):

```diff
diff --git a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
index 5ebb09a..18d7825 100644
--- a/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
+++ b/src/Symfony/Component/Security/Core/Authentication/Provider/LdapBindAuthenticationProvider.php
@@ -70,7 +70,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
      */
     protected function checkAuthentication(UserInterface $user, UsernamePasswordToken $token)
     {
-        $username = $token->getUsername();
+        $username = $user->getUsername();
         $password = $token->getCredentials();

         if ('' === $password) {
@@ -78,10 +78,7 @@ class LdapBindAuthenticationProvider extends UserAuthenticationProvider
         }

         try {
-            $username = $this->ldap->escape($username, '', LdapInterface::ESCAPE_DN);
-            $dn = str_replace('{username}', $username, $this->dnString);
-
-            $this->ldap->bind($dn, $password);
+            $this->ldap->bind($username, $password);
         } catch (ConnectionException $e) {
             throw new BadCredentialsException('The presented password is invalid.');
         }
diff --git a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
index fc42419..8194c4c 100644
--- a/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
+++ b/src/Symfony/Component/Security/Core/User/LdapUserProvider.php
@@ -115,7 +115,7 @@ class LdapUserProvider implements UserProviderInterface
     {
         $password = $this->getPassword($entry);

-        return new User($username, $password, $this->defaultRoles);
+        return new User($entry->getDn(), $password, $this->defaultRoles);
     }

     /**
```

Therefore I created an entire new auth provider.

Commits
-------

8ddd533 Merge pull request #1 from nietonfir/http_basic_ldap
a783e5c Update HttpBasicLdapFactory
a30191f make LdapBindAuthenticationProvider capable of searching for the DN
lsmith77 pushed a commit to lsmith77/symfony-docs that referenced this pull request Feb 3, 2017
xabbuh added a commit to symfony/symfony-docs that referenced this pull request Feb 28, 2017
…reguiluz, lsmith77)

This PR was merged into the master branch.

Discussion
----------

Added query_string LDAP config option

docs for symfony/symfony#21402

Commits
-------

b82cafd clean up
446ba38 added query_string LDAP config option
ed58da8 Minor reword
f133269 Explain the query_string ldap authentication provider configuration key
@nicolas-grekas nicolas-grekas modified the milestones: 3.x, 3.3 Mar 24, 2017
@fabpot fabpot mentioned this pull request May 1, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants