Sunday, July 31, 2011

LDAP authentication using PHP

LDAP can be good alternative authentication tool for your organization.. This has several advantages. It allows to manage user authentication information centrally. Users may not require to create new account every time a new system is deployed organization-wide given that the application being deployed supports LDAP authentication. In the rest of the tutorial I am going to describe the way I prefer to implement LDAP authentication in PHP.

You will need a service account in the LDAP server which the authentication system will use to login and search and verify the user trying to login. lets define the config variables we will be using:
$service_account_user = 'xxx';
$service_account_pass = 'yyy';
$base_dn              = 'dc=example,dc=com';
$ldap_server          = 'xyz.com';
$ldap_port            = 389;
$ldap_version         = 3;
$user_filter          = 'sAMAccountName=%USERNAME%';

In the following process, We are going to update application's own database too in case of successful authentication  so that users can login even if the LDAP server is offline. You may have reservation for this method that users who have been removed from LDAP server are still able to login. yes, that's true but its very simple to address.
lets track whether application's own authentication system should be used, using the following variable:
 
$fallback = false;
lets name the function we will be writing as "authenticate":

function authenticate ($username, $password){ 
    $fallback = false; 
}
which will return an array with two fields.
return array(
       'status'=> true, //bolean: true for success, false for failure
       'message'=>''    // string: one description of what actually happened.
     );

step 01:

check if the password is provided. if not then return immediately telling that authentication process failed. Because LDAP will succeed in binding with no password (which is in this case an anon bind).
if(strlen($password) == 0){
    return array(
        'status'=>false,
        'message' => 'Can't authenticate with empty password'
    );
}

step 02:

now lets try to connect to ldap server. if connection fails then we'll fallback. If connection succeeds then we'll try to "bind" using service user/pass, set search parameters and search for the user trying to login.
if (!$rs = ldap_connect($ldap_server, $ldap_port)) {
    $fallback = true;
}
else{
    $ldap_service_dn   = 'CN='.$service_account_user.',OU=users,'.$base_dn;
    $ldap_service_pass = $service_account_pass;        
    ldap_set_option($rs, LDAP_OPT_PROTOCOL_VERSION, $ldap_version);
    ldap_set_option($rs, LDAP_OPT_REFERRALS, 0);            
    if (!$bindok = ldap_bind($rs, $ldap_service_dn, $ldap_service_pass)) {
        //LDAP bind did not success. lets fallback
        $fallback = true;
    } else {
        $entity = str_replace('%USERNAME%', $username, $user_filter);
        $user_filter = html_entity_decode($entity, ENT_COMPAT, 'UTF-8');
        $result = ldap_search($rs, $this->base_dn, $user_filter);

        if (!$result) {
            // LDAP search failed. do fallback authentication.
            $fallback = true;
        }
        else{
            $user_info = ldap_get_entries($rs, $result);
            if ($user_info['count'] == 0) {
                // Didn't find the user in LDAP server. so disallow login.
                return array(
                    'status'=>0,
                    'message'=>'User could not be found.'
                ); 
            }

            $found_user = $user_info[0];
            $ldap_user_dn = $found_user['dn'];

            // user is found. now lets check weather the password given is valid or not. 
            // bind with the combination of password given and the DN 
            // returned by the search operation.
            if (!$bind_user = ldap_bind($rs, $ldap_user_dn, $password)) {
                return array(
                    'status'=>0,
                    'message'=>'User/pass combination is not valid.'
                ); 
            }
            // all good. now check if the user allready exists in application's database. 
            // if not then create one.
            if (user_exists($username, $password, $found_user)) {
                return array(
                    'status'=>1,
                    'message'=>'Login successful.'
                ); 
            } else {
                create_sql_user($username, $password, $found_user);
                return array(
                    'status'=>1,
                    'message'=>'First time login.'
                );                         
            }                    
        }        
    }            
}
//if fallback authentication is required then now is the right time to do that.
if($fallback)
    if(fallback_authenticate()){
        return array(
            'status'=>1,
            'message'=>'Fallback authentication failed.'
        );                                     
    }
    else{
        return array(
            'status'=>0,
            'message'=>'Fallback authentication failed.'
        );                                     
    }
}
step 03:

Define user_exists function which will return true or false. in addition, You can update user info which has been in LDAP server.

step04:

Define create_sql_user function.  Use the user info return by LDAP server to fill out as necessary.

step 05:

Define fallback_authenticate  function and you are done :)