-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Conversation
* @author Grégoire Pineau <[email protected]> | ||
* @author Charles Sarrazin <[email protected]> | ||
*/ | ||
class FormLoginLdapFactory extends FormLoginFactory |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 ;).
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
@@ -48,6 +48,7 @@ | |||
"symfony/expression-language": "For using the expression voter", | |||
"ircmaxell/password-compat": "For using the BCrypt password encoder in PHP <5.5" | |||
}, | |||
"minimum-stability": "dev", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this really required?
Can you add a suggests for the LDAP in here?
http://php.net/manual/en/ldap.requirements.php
Maybe it's an idea to throw an exception in the SecurityExtension if LDAP is configured but not installed on the system?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the minimum-stability, I don't think so. It's simply a remnant of the previous PR, so I guess I'll fix that. No problem for the LDAP suggest (for the extension), too. :)
As for the SecurityExtension triggering an exception if the LDAP extension is not installed, I don't think it's necessary. If you wish to, you can use the PEAR LDAP2 implementation, which is a full-PHP implementation. And the default implementation (which uses the LDAP extension) already throws an exception if the extension is not loaded. :)
@jakzal You can also add the |
Ok. I'm running into an issue: Should LDAP classes be in Any advice on this, @weaverryan, @stof, @iltar? |
If the LDAP connection and such is generic enough, it might not belong in the Security component at all. I'm still not satisfied with how this is going to be configured though, fetching your user from LDAP should be nothing more than changing your user provider. This will make it possible to use it with any authentication provider. |
It's already the case. Two things are implemented in this PR:
Each of them are independent from each other. This means that you can fetch your users from your LDAP server, while checking their password as you would normally (should your LDAP provide the password hash), using ANY authentication provider. But in this case, you won't check the user's password using If you wish to check the user's password against an LDAP server, then you use the authentication provider. This provider lets you use any user provider, while checking the user's password against an LDAP server. This is the case because of Symfony's security component suffers from a limitation, which is that the password check mechanism is directly implemented inside the authentication providers. And I don't want to adress two things in a single PR. First, LDAP support. Second, refactor the security component, to separate the password check from the authentication providers. If you feel that this is really needed now, I'll separate this PR into two different PRs:
What do you think about this? |
The user providers meant to merely provide a user to authenticate, when you fetch the user, it contains a password. This authentication is method is fine and the password check IMO as well as not every provider needs a password. To verify if the user is correct, you simply verify the password with the encoding strategy selected by the configuration, which will be "plain" most likely. You're correct that it's absolutely not desired to store the password (plain) in the user as this is very risky. There's a solution for this, you'll have to implement the If you need to call another method, you can always listen to an event that's fired when the login was successful and bind there. |
Except LDAP auth doesn't work like this. Most of the time you don't have access to the user password, instead you connect to the server with the user credential and check if the login attempt succeeded or not. |
Ping @fabpot Should we have an LDAP component? |
@csarrazi If the classes are useful by themselves and are not trivial, I would say yes. |
Ok. This would indeed resolve part of the issue, which would let us add the component in the The class is not trivial by itself, as it provides a shim for |
Did you already work with LDAP ? Because it looks like it's not the case. Symfony Security default workflow does not work with LDAP Authentication workflow. |
@lyrixx I have worked with LDAP (and different ldap bundles), but I'm also looking from a DX point of view. There should be enough extension points to solve this while leaving it 'configurable' by the developer. |
There are two different concerns here:
These are two different things, and this PR works with any combination of these use cases:
As mentioned earlier, these are all different use cases. You may want to authenticate against your LDAP server, while fetching users from a database. Just like you would, if you wanted to fetch users from a local database, while validating the user's credentials from an OAuth 2.0 identity provider. Or you may want to fetch your users from an LDAP server. In this case, you can check the user's credentials using one of two strategies:
Like mentioned earlier, if we want to avoid duplication (and duplicating the authentication providers), we would need to separate the authentication check logic from the authentication provider (http-basic, etc.), so we can use different authentication check types, with a single authentication scheme (http-basic, login form, etc.). However, we need to think a bit more about this, as it would probably induce BC with the |
*/ | ||
protected function retrieveUser($username, UsernamePasswordToken $token) | ||
{ | ||
return $this->userProvider->loadUserByUsername($username); |
There was a problem hiding this comment.
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');
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oups. Yes.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
Does someone know how to setup travis + a ldap server ? |
return; | ||
} | ||
|
||
$ldap = new LdapClient(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would use setUp
method to create the SUT. But it's a matter of taste.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would have done this, if there were more than one test method ;)
Tests seem okay. However, some unrelated tests seem to break randomly on travis and/or appveyor. |
Sorry about that but it looks like a rebase is needed. |
No problem! I'll also include @lyrixx 's changes regarding the |
@@ -35,6 +35,7 @@ before_install: | |||
- if [[ "$TRAVIS_PHP_VERSION" = 5.* ]]; then (pecl install -f memcached-2.1.0 && echo "extension = memcache.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini) || echo "Let's continue without memcache extension"; fi; | |||
- if [[ "$TRAVIS_PHP_VERSION" = 5.* ]] && [ "$deps" = "no" ]; then (cd src/Symfony/Component/Debug/Resources/ext && phpize && ./configure && make && echo "extension = $(pwd)/modules/symfony_debug.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini); fi; | |||
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then php -i; fi; | |||
- if [[ "$TRAVIS_PHP_VERSION" != "hhvm" ]]; then echo "extension = ldap.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The LDAP extension is available without any installation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It seems that the LDAP extension is available by default in Travis' PHP build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
indeed, 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, and this is certain, as I added the skip tests only after checking that they passed on travis first, and because of appveyor launching tests with and without PHP extensions.
Thank you @csarrazi. |
…ntegration in the Security Component). (csarrazi, lyrixx) This PR was merged into the 2.8 branch. Discussion ---------- [2.8] [Ldap] Added support for LDAP (New Component + integration in the Security Component). | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | no | Deprecations? | no | Tests pass? | not yet | Fixed tickets | - | License | MIT | Doc PR | not yet Current state: - [x] Implement logic - [x] Post-review tuning and stabilization - [x] Fix tests This PR is a follow-up to #5189, which was in a stand-still for a few years now. It tries to fix the remaining issues which were mentioned in the discussion. There are still a few issues with the PR, as it is. For example, it introduces two new firewall factories, whereas the base factories (`form_login` and `http_basic`) could simply introduce new configuration options. Also, for a user to use an LDAP server as an authentication provider, he first needs to define a service which should be an instance of `Symfony\Component\Security\Ldap\Ldap`. For example: ```yml services: my_ldap: class: Symfony\Component\Security\Ldap\Ldap arguments: [ "ldap.mydomain.tld" ] ``` Then, in `security.yml`, this service can be used in both the user provider and the firewalls: ```yml security: encoders: Symfony\Component\Security\Core\User\User: plaintext role_hierarchy: ROLE_ADMIN: ROLE_USER ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH] providers: ldap_users: ldap: service: my_ldap base_dn: dc=MyDomain,dc=tld search_dn: CN=My User,OU=Users,DC=MyDomain,DC=tld search_password: p455w0rd filter: (sAMAccountName={username}) default_roles: ROLE_USER firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false demo_login: pattern: ^/login$ security: false api: provider: ldap_users stateless: true pattern: ^/api http_basic_ldap: service: my_ldap dn_string: "{username}@mydomain" demo_secured_area: provider: ldap_users pattern: ^/ logout: path: logout target: login form_login_ldap: service: my_ldap dn_string: CN={username},OU=Users,DC=MyDomain,DC=tld check_path: login_check login_path: login ``` Commits ------- 60b9f2e Implemented LDAP authentication and LDAP user provider 1c964b9 Introducing the LDAP component
@@ -0,0 +1,49 @@ | |||
<?php | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
header class is missing
This PR was merged into the 2.8 branch. Discussion ---------- [Ldap] add some missing license file headers | Q | A | ------------- | --- | Bug fix? | no | New feature? | no | BC breaks? | no | Deprecations? | no | Tests pass? | yes | Fixed tickets | #14602 (comment) | License | MIT | Doc PR | Commits ------- 2b90fcf [Ldap] add some missing license file headers
* @param bool $useStartTls | ||
* @param bool $optReferrals | ||
*/ | ||
public function __construct($host = null, $port = 389, $version = 3, $useSsl = false, $useStartTls = false, $optReferrals = false) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's impossible to specify additional options, such as timeout, debug level, etc... As this component should be used as a standalone, I suggest to put options here or make an interface to adjust each option, available for ldap_set_option
Example:
$ldap = new LdapClient();
$ldap->setNetworkTimeout(10);
$ldap->setDebugLevel(7);
$ldap->setUseSsl(true);
$ldap->setProtocolVersion(3);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer a configuration object per connection, it's possible to have multiple connections. Optionally the Option component could be used to build this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@iltar yes, nice catch. In my case we have connections to the several hosts and one "aggregated" client to query an information. But this LdapClient
is simple enough and should be used as single entry point for single connection.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could separate the Client from the Connection implementation, if needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Having a configuration object seems like a good idea. The long list of arguments here is indeed a problem.
return $this->doEscape($subject, $ignore, $flags); | ||
} | ||
|
||
private function connect() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think that this method should be protected, or, at least, allow to configure all options for connection.
Why I want to make it protected: I can define a class with reconnection logic in case I needed a fail-tolerance solution, additionally, we always enable timeouts for connections (1second)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm repeating myself, but the connection logic will be moved out from the client. So indeed, you will be able to configure all connection options (as well as connection handling) the Connection class.
This PR was merged into the 3.1-dev branch. Discussion ---------- [Ldap] Improving the LDAP component | Q | A | ------------- | --- | Bug fix? | no | New feature? | yes | BC breaks? | yes | Deprecations? | no | Tests pass? | no | Fixed tickets | #14602 | License | MIT | Doc PR | not yet This PR will address a few issues mentioned in #14602. * [x] Integrate the Config component in order to simplify the client's configuration * [x] Separate Connection handling from the Client * [x] Support for multiple drivers * [x] Add functional tests * [x] Update Security component Commits ------- 34d3c85 Added compatibility layer for previous version of the Security component 81cb79b Improved the Ldap Component
@csarrazi Would it be possible to use the same login credential for both form_login_ldap authentication provider and ldap user provider? No sure if this has been addressed before, but I couldn't find any information about it after googling a few days. This LDAP component seems requiring 2 LDAP accounts to authenticate users: a fixed site-wise admin account as search_dn for ldap user provider, and a user account to be logging in for the form_login_ldap. This would cause issue if it is not possible to get a fixed admin credential from the organisation or if the admin password would change regularly. Seeing other framework dealing with ldap authentication only requires one ldap user credential, I wonder if this ldap component has a way to do the same? Please advice, thanks in advance. |
Hi @jianzhong. The Ldap component itself does not offer any limitation of this kind. With your own logic, you could actually build your own custom authenticator, which could bind to the LDAP server, then search for a user with the same credentials using a custom user provider. This type of feature may be implemented within the security component in some time. For that, we could provide an option to disable re-binding the user in the user provider, and provide another option to configure when to bind the user against the ldap server. Also, this would only work in a few select cases:
Re-binding the same user against the ldap server between two different requests without asking for credentials won't (and shouldn't) be supported. This mostly has to do with the security component, and how PHP works in general, when using apache's mod_php or php-fpm. Whenever a request is sent to the server, the whole userland context (what the PHP script is doing) is discarded, and flushed from memory. Many database extensions (MySQL or MongoDB, for example) actually implement connection pooling and/or provide the ability to keep connections alive between requests, but it is not the case for PHP's LDAP extension. |
Thanks @csarrazi for your reply and pointing me to the right direction. |
Current state:
This PR is a follow-up to #5189, which was in a stand-still for a few years now. It tries to fix the remaining issues which were mentioned in the discussion.
There are still a few issues with the PR, as it is. For example, it introduces two new firewall factories, whereas the base factories (
form_login
andhttp_basic
) could simply introduce new configuration options.Also, for a user to use an LDAP server as an authentication provider, he first needs to define a service which should be an instance of
Symfony\Component\Security\Ldap\Ldap
.For example:
Then, in
security.yml
, this service can be used in both the user provider and the firewalls: