Reputation: 26170
i've this function bellow for authenticate on my active directory, it's working fine except for some passwords containing special chars like this one :
Xefeéà&"+
which always return 'invalid credentials'
I've seen several posts on internet but none seems to correctly escape the chars.
i'm working with PHP Version 5.3.8-ZS (Zend server)
here are the settings in my class constructor :
ldap_set_option($this->_link, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($this->_link, LDAP_OPT_REFERRALS, 0);
And the login function :
public function login($userlogin,$userpassword){
$return=array();
$userlogin.="@".$this->_config["domaine"];
$ret=@ldap_bind($this->_link , $userlogin ,utf8_decode($userpassword));
if(!$ret){
$return[]=array("status"=>"error","message"=>"Erreur d'authentification ! <br/> Veuillez vérifier votre nom et mot de passe SVP","ldap_error"=>ldap_error($this->_link));
}
if($ret)
{
$return[]=array("status"=>"success","message"=>"Authentification réussie");
}
return json_encode($return);
}
Any help will be greatly appreciated.
Upvotes: 3
Views: 10351
Reputation: 392
You may want to use LDAPv3
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
LDAPv3 supports UTF-8 by default, which it expects requests and responses to be in by default.
This solved my issues with password having "€" or "@" or "#"
Upvotes: 0
Reputation: 1
Be extremely careful with the ldap_compare
! It's written everywhere not to use it for binary values because in some environments example AD: it returns always true
.
https://www.php.net/manual/en/function.ldap-compare.php
Warning
ldap_compare()
can NOT be used to compare BINARY values!
Upvotes: 0
Reputation: 1096
We found ourselves in a similar siutaion where our enterprise environment LDAP authentication setup for one of our internally hosted web applications seemed to be working perfectly, until users who had special characters in their passwords - characters like ? & * ' - wanted to use the application and it then became apparent that something was not actually working as expected.
We discovered that our authentication requests failed at the call to ldap_bind()
when special characters were present in affected users' passwords, and it was common to see errors such as Invalid credentials
or NDS error: failed authentication (-669)
(from LDAP's extended error logging) being output to the logs. In fact it was the NDS error: failed authentication (-669)
error that led to the discovery that the failed binds were likely due to the presence of special characters in passwords - via this Novell Support article https://www.novell.com/support/kb/doc.php?id=3335671 with the most notable part excerpted below:
Admin password had characters that LDAP does not understand, ie:"_" and "$"
A range of fixes had already been extensively tested including 'cleaning' the password through html_entity_decode()
, utf8_encode()
, etc, and ensuring that no mis-encoding of the password had occurred between the web application and the various servers, but no matter what was done to adjust or protect the input text to ensure its original UTF-8 encoding remained intact, ldap_bind()
always failed when one or more special characters was present in the password (we didn't test cases with special characters in the username, but others have reported similar issues when the username contains special characters instead of or in addition to the password).
We also tested ldap_bind()
directly with a minimal PHP script run on the command line with a hard-coded username and password for a test LDAP account that had special characters in the password (including question marks, asterisks, and exclamation marks), in an attempt to eliminate as many potential causes of failure as possible, but the bind operation still failed. Thus it was clear that it was not an issue of the password being mis-encoding between the web application and the server, all of which were designed and built to be UTF-8 compliant end-to-end. Rather it appeared that there was an underlying issue with ldap_bind()
and its handling of special characters. The same test script succeeded to bind for another account that did not have any special characters in the password, further confirming our suspicions.
The earlier version of LDAP authentication worked as below, as most of the PHP LDAP authentication tutorials recommend:
$success = false; // could we authenticate the user?
if(!defined("LDAP_OPT_DIAGNOSTIC_MESSAGE")) {
define("LDAP_OPT_DIAGNOSTIC_MESSAGE", 0x0032); // needed for more detailed logging
}
if(($ldap = ldap_connect($ldap_server)) !== false && is_resource($ldap)) {
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
if(@ldap_bind($ldap, sprintf("cn=%s,ou=Workers,o=Internal", $username), $password)) {
$success = true; // login succeeded...
} else {
error_log(sprintf("* Unable to authenticate user (%s) against LDAP!", $username));
error_log(sprintf(" * LDAP Error: %s", ldap_error($ldap)));
if(ldap_get_option($ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
error_log(sprintf(" * LDAP Extended Error: %s\n", $extended_error));
unset($extended_error);
}
}
}
@ldap_close($ldap); unset($ldap);
As it was clear that our simpler solution using just ldap_connect()
and ldap_bind()
would not allow users with complex passwords to authenticate, we had to find an alternative.
We opted for a solution based on first binding to our LDAP server using a system account (created with the necessary privileges to search for users, but intentionally configured as a limited, read-only account). We then searched for the user account with the provided username using ldap_search()
, then used ldap_compare()
to compare the user's provided password with that stored in LDAP. This solution has proven to be reliable and effective, and we are now able to authenticate users with special characters in their passwords just as well as those without!
The new LDAP process works as follows:
if(!defined("LDAP_OPT_DIAGNOSTIC_MESSAGE")) {
define("LDAP_OPT_DIAGNOSTIC_MESSAGE", 0x0032); // needed for more detailed logging
}
$success = false; // could we authenticate the user?
if(($ldap = @ldap_connect($ldap_server)) !== false && is_resource($ldap)) {
ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
// instead of trying to bind the user, we bind to the server...
if(@ldap_bind($ldap, $ldap_username, $ldap_password)) {
// then we search for the given user...
if(($search = ldap_search($ldap, "ou=Workers,o=Internal", sprintf("(&(uid=%s))", $username))) !== false && is_resource($search)) {
// they should be the first and only user found for the search...
if((ldap_count_entries($ldap, $search) == 1) && ($entry = ldap_first_entry($ldap, $search)) !== false && is_resource($entry)) {
// we ensure this is the case by obtaining the user identifier (UID) from the search which must match the provided $username...
if(($uid = ldap_get_values($ldap, $entry, "uid")) !== false && is_array($uid)) {
// ensure that just one entry was returned by ldap_get_values() and ensure the obtained value matches the provided $username (excluding any case-differences)
if((isset($uid["count"]) && $uid["count"] == 1) && (isset($uid[0]) && is_string($uid[0]) && (strcmp(mb_strtolower($uid[0], "UTF-8"), mb_strtolower($username, "UTF-8")) === 0))) {
// once we have compared the provied $username with the discovered username, we get the DN for the user, which we need to provide to ldap_compare()...
if(($dn = ldap_get_dn($ldap, $entry)) !== false && is_string($dn)) {
// we then use ldap_compare() to compare the user's password with the provided $password
// ldap_compare() will respond with a boolean true/false depending on if the comparison succeeded or not, and -1 on error...
if(($comparison = ldap_compare($ldap, $dn, "userPassword", $password)) !== -1 && is_bool($comparison)) {
if($comparison === true) {
$success = true; // user successfully authenticated, if we don't get this far, it failed...
}
}
}
}
}
}
}
}
if(!$success) { // if not successful, either an error occurred or the credentials were actually incorrect
error_log(sprintf("* Unable to authenticate user (%s) against LDAP!", $username));
error_log(sprintf(" * LDAP Error: %s", ldap_error($ldap)));
if(ldap_get_option($ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extended_error)) {
error_log(sprintf(" * LDAP Extended Error: %s\n", $extended_error));
unset($extended_error);
}
}
}
@ldap_close($ldap);
unset($ldap);
One of the original concerns was that this new approach, with all the extra steps would take much longer to perform, but it turned out (at least in our environment) that the timing differences between running a simple ldap_bind()
and the more complex solution using ldap_search()
and ldap_compare()
were actually insignificant, being perhaps 0.1s longer on average.
As a note, both the earlier and latter versions of our LDAP authentication code should only be run after user input data (i.e. username and password input) has been verified and tested to ensure no empty or invalid username or password strings have been provided, and that no invalid characters (such as null bytes) are present. Additionally, user credentials should ideally be sent via a secure HTTPS request from the application, and the secure LDAPS protocol can be used to further protect user credentials during the authentication process. We found using LDAPS directly, connecting to the server via ldaps://...
rather than connecting via ldap://...
and then switching over to a TLS connection has proven to be more reliable. If your setup varies, the code above will need to be adjusted accordingly to incorporate support for ldap_start_tls()
.
Lastly, it may be worth noting that the LDAP protocol requires passwords to be encoded using UTF-8 as per the RFC (RFC 4511, p16, first paragraph) – so if there are cases where any of the systems being used during a request are not handling the user's credentials as UTF-8 from initial entry all the way through to authentication, that could also be an area worth investigating.
Hopefully this solution will prove useful to others who have struggled to solve this issue with many of the other reported solutions, but still found cases where users could not be authenticated. The code may need to be adjusted for your own LDAP environment, especially the DN used in ldap_search()
as that will depend on how your LDAP directory is configured and which user groups you are supporting for your application. Gladly, it is working very well in our environment, and hopefully the solution will prove as useful elsewhere.
Upvotes: 9
Reputation: 41
Try html_entity_decode
for the password instead of utf8_decode; it was the only thing that worked for me.
Upvotes: 4