8000 [Ldap] Use discovered DNs for the authentication bind · Issue #16823 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[Ldap] Use discovered DNs for the authentication bind #16823

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

Closed
junowilderness opened this issue Dec 3, 2015 · 11 comments
Closed

[Ldap] Use discovered DNs for the authentication bind #16823

junowilderness opened this issue Dec 3, 2015 · 11 comments

Comments

@junowilderness
Copy link
Contributor

In implementing LDAP authentication in 2.8, we have found that authentication is limited to only users whose DNs fit the configured dn_string pattern. Let us consider this configuration:

security:
    providers:
        in_memory:
            memory: ~
        app_users:
            ldap:
                service: app.ldap
                base_dn: %ldap.base_dn%
                search_dn: null
                search_password: null
                filter: (uid={username})
                default_roles: ROLE_USER
    firewalls:
        secure:
            provider:  app_users
            stateless: true
            pattern:   ^/secure
            http_basic_ldap:
                service: app.ldap
                dn_string: "uid={username},ou=people,dc=example,dc=com"
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: ~

In this case, a user with DN "uid=foo,ou=people,dc=example,dc=com" can authenticate but one with DN "uid=foo,ou=something,ou=people,dc=example,dc=com" cannot, even though both accounts are in the configured search base.

The immediate reason is that after discovering information from LDAP with a call to ->find() in the LdapUserProvider::loadUserByUsername() method, the DN is discarded when LdapUserProvider::loadUser() creates the User instance.

Because the DN is discarded, later LdapBindAuthenticationProvider::checkAuthentication() recombines the basic username with what is configured in dn_string:

$dn = str_replace('{username}', $username, $this->dnString);

It would seem if User were extended to store the DN (perhaps by creating LdapUser with a setDn() method), it would not be necessary to set dn_string, and users in various OU's could authenticate.

@junowilderness
Copy link
Contributor Author

PR, in-progress, still needs work #16827

@xabbuh xabbuh added the Ldap label Dec 10, 2015
@csarrazi
Copy link
Contributor

As mentioned in your PR, I would rather have a dedicated LdapUser class, in which the logic would be encapsulated.

For your information, LDAP implementations in the framework are independent from each other. This means that you can use the Ldap user provider with a different check mechanism (password comparison, and not necessarily ldap_bind), or the ldap_bind-based authentication mechanisms with a third-party user storage (database, or other).

@csarrazi
Copy link
Contributor

I'm mentioning PR #17813 as another way to improve DN management using a previous search.

I will start from your current PR (#16827), improving it and implement both approaches as a single feature. Unfortunately, this will have to wait until next week, as I first intend to work on the documentation for the Ldap component, and a cookbook explaining how to use the Ldap component.

@ofbeaton
Copy link
ofbeaton commented May 3, 2016

I ran into this problem myself while debugging why I was getting a Invalid credentials. In my case it is even more problematic as the dn_string is something like CN=Finlay Beaton,OU=Users_ORG1,DC=MyNetwork,DC=local while the sAMAccountName used to authenticate is something like fxb456. I would rather search within all of DC=MyNetwork,DC=local like I do in my provider filter (which correctly finds my user) to support multiple OU's.

Moreover the dn_string of CN={username},OU=Users_ORG1,DC=MyNetwork,DC=local fails for me because {username} is not the right key, I'm a bit of a noob so I'm not even sure if there is a way for me to have the dn_string be the right field for me...

EDIT SOLVED: Thank you for your linked PR #17813 comment, from there I was able to know that I could change the dn_string to {username}@MyNetwork, which worked for the dn_string part. I'm using Active Directory with a specified search_dn.

EDIT 2: For those trying to troubleshoot Active Directory problems, I found this extra debugging information invaluable. I editing the vendor code and added this information (per this php comment)

ldap_get_option($handle, 0x0032, $extended_error);

That gives you extra information on AD errors, so I was able to get data 525 which led me to this table about LDAP error no. 49 with data 525 being user not found based on the table in ibm support ticket

@krisdante
Copy link

Wouldn't it be better to extend LdapBindAuthenticationProvider the way how LdapUserProvider is configured already - so it accepts searchDn with the password, and then filter to find user's DN based on condition like (uid={username})?
This way Authentication will be independent of Provider.

If you think that would be reasonable I can implement that.

@lsmith77
Copy link
Contributor

@csarrazi I assume you are not actively working on this topic after all?

@csarrazi
Copy link
Contributor

@lsmith77 Unfortunately, not on this specific topic. The component is only in its infancy, so there's still a lot of work to do, and I do not have much time to work on this.

Heck, I don't even have time to finalise the docs PR for 3.1!

So, contributions are always welcome! :)

@nietonfir
Copy link

In case someone is interested: I have an OpenLDAP instance configured with the meta backend as a proxy for two ldap directories (with distinct uid's!). As such, users have two different dn (ou=companyA,ou=users,dc=local vs ou=companyB,ou=users,dc=local). The user provider logic is fairly simple, in my case I decided to use a chained provider for two ldap providers for the same host but with different baseDN.

I extended my custom User model with a getter & setter for the dn. In my overriden LdapUserProvider the dn gets set in loadUser:

namespace AppBundle\Security\Core\User;

use AppBundle\Model\User;
use Symfony\Component\Security\Core\User\LdapUserProvider as BaseProvider;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use Symfony\Component\Ldap\Entry;
use Symfony\Component\Ldap\LdapInterface;

class LdapUserProvider extends BaseProvider
{

    private $defaultRoles;

    public function __construct(LdapInterface $ldap, $baseDn, array $defaultRoles = array())
    {
        $this->defaultRoles = $defaultRoles;

        parent::__construct($ldap, $baseDn, null, null, $defaultRoles, 'uid');
    }

    /**
     * {@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 === User::class;
    }

    protected function loadUser($username, Entry $entry)
    {
        $user = new User($username, null, $this->defaultRoles);
        $user->setDn($entry->getDn());

        return $user;
    }
}

The stored dn is then retrieved by the registered authenticator:

namespace AppBundle\Security\Http\Authentication;

use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Ldap\Ldap;
use Symfony\Component\Ldap\Exception\BadCredentialsException;
use Symfony\Component\Ldap\Exception\ConnectionException;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authentication\Token\UsernamePasswordToken;
use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\User\UserProviderInterface;
use Symfony\Component\Security\Http\Authentication\SimpleFormAuthenticatorInterface;

class LdapAuthenticator implements SimpleFormAuthenticatorInterface
{
    private $ldap;

    public function __construct(Ldap $ldap)
    {
        $this->ldap = $ldap;
    }

    public function authenticateToken(TokenInterface $token, UserProviderInterface $userProvider, $providerKey)
    {
        try {
            $user = $userProvider->loadUserByUsername($token->getUsername());
        } catch (UsernameNotFoundException $e) {
            throw new CustomUserMessageAuthenticationException('Invalid username or password');
        }

        try {
            $this->ldap->bind(); // or $ldap->bind($searchDn, $searchPassword);
        } catch (ConnectionException $e) {
            throw new BadCredentialsException('Could not connect to server');
        }

        try {
            $this->ldap->bind($user->getDn(), $token->getCredentials());
        } catch (ConnectionException $e) {
            throw new BadCredentialsException('Bad password');
        } catch (LdapException $e) {
            throw new BadCredentialsException('Bad query');
        }

        return new UsernamePasswordToken(
            $user,
            $user->getPassword(),
            $providerKey,
            $user->getRoles()
        );
    }

    public function supportsToken(TokenInterface $token, $providerKey)
    {
        return $token instanceof UsernamePasswordToken
            && $token->getProviderKey() === $providerKey;
    }

    public function createToken(Request $request, $username, $password, $providerKey)
    {
        return new UsernamePasswordToken($username, $password, $providerKey);
    }
}

The authenticator is slightly different than suggested by @csarrazi in #20905, as I don't see the point in a repeated query if I already have everything I need as the authenticator would have to be configured just like the providers to be able to find the users. And that shouldn't be necessary imho.

@csarrazi
Copy link
Contributor

@nietonfir As mentioned before, a repeated query should not be necessary in all cases. However, keep in mind that you may have access to different attributes depending on the user, or search may actually be disabled for users other than the search user, based on the access controls enforced by the server.

Indeed, in many setups, human users may not have the ability to search other users' information in the LDAP tree. Likewise, the search user may not have the ability to see certain attributes which can be seen by a user of the application (an email, for example, may only be visible to the user, and not the search user).

In the longer term, I think that we should have a configuration option which will define the behaviour of the user provider, as well as the authenticator.

A.k.a:

  • Find user's DN before bind (fetch only the DN attribute) using the search user
  • Bind before fetching user's attributes with the user provider (fetch all attributes), using either the search user, or the user's credentials (this would mean having a higher level service on top of the Ldap component, responsible for storing the search user's credentials, so they can be used by both the user provider or the authenticator).

By the way @nietonfir, you'll be happy to know that I'm currently working on providing a means to use multiple DNs for users. Currently, the design is still in WIP, but it could both replace the search before bind in a a lot of cases, or the use of a chain user provider.

@nietonfir
Copy link

@csarrazi You're totally right, I didn't think about that use case. Can't wait to see your solution. ;-) Is there anything one could help with?

@lsmith77
Copy link
Contributor

see #21402

fabpot added a commit that referenced this issue 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
@fabpot fabpot closed this as completed Jan 28, 2017
chalasr pushed a commit to chalasr/symfony that referenced this issue 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

9 participants
0