LDAP Authentication for Cakephp

Header

This article is going to help you using LDAP to authenticate users rather than relying on a users table with a password column. I will be assuming you will be using cakephp 1.3 and that you have completed Auth and/or ACL setup on your application similar to the ACL tutorial on the cakephp book.

Because we want to control the logging in of the user ourselves and not leave it to the cake magic we need to override the auth component. To do this copy your auth.php from your CAKE_CORE/controllers/components/ to your APP/controllers/components/ folder. Next open it up and fine the login function. It should be around like 684. Once you find it comment out everything inside the function, but leave the function intact. It should look something like this:

function login($data = null) {  
    /*$this->__setDefaults();
    $this->_loggedIn = false;
    if (empty($data)) {$data = $this->data; }
    if ($user = $this->identify($data))
    { $this->Session->write($this->sessionKey, $user);
    $this->_loggedIn = true; }
return $this->_loggedIn;*/ }  

Next open up your users controller and find your login function. Assuming you followed the guide or have implemented some basic auth you should have an empty login function.

I have written a LDAP helper that can easily be included as a LIB for Cakephp that will get some user data and validate the login, copy and paste it from below to APP/libs/ldap.php

<?php  
class ldap{

    private $ldap = null;
    private $ldapServer = 'AD.domain.com';
    private $ldapPort = '389';
    public $suffix = '@domain.com';
    public $baseDN = 'dc=domain,dc=com';
    private $ldapUser = 'LDAPUser';
    private $ldapPassword = 'Pas5w0rd';

    public function  __construct() {
        $this->ldap = ldap_connect($this->ldapServer,$this->ldapPort);

        //these next two lines are required for windows server 03
        ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, 0);
        ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
    }

    public function auth($user,$pass)
    {
        if (empty($user) or empty($pass))
        {
            return false;
        }
        @$good = ldap_bind($this->ldap,$user.$this->suffix,$pass);
        if( $good === true ){
            return true;
        }else{
            return false;
        }
    }

    public function __destruct(){
        ldap_unbind($this->ldap);
    }

    public function getInfo($user){
        $username = $user.$this->suffix;;
        $attributes = array('givenName','sn','mail','samaccountname','memberof');
        $filter = "(userPrincipalName=$username)";

        ldap_bind($this->ldap,$this->ldapUser.$this->suffix,$this->ldapPassword);
        $result = ldap_search($this->ldap, $this->baseDN, $filter,$attributes);
        $entries = ldap_get_entries($this->ldap, $result);

        return $this->formatInfo($entries);
    }

    private function formatInfo($array){
        $info = array();
        $info['first_name'] = $array[0]['givenname'][0];
        $info['last_name'] = $array[0]['sn'][0];
        $info['name'] = $info['first_name'] .' '. $info['last_name'];
        $info['email'] = $array[0]['mail'][0];
        $info['user'] = $array[0]['samaccountname'][0];
        $info['groups'] = $this->groups($array[0]['memberof']);

        return $info;
    }

    private function groups($array)
    {
        $groups = array();
        $tmp = array();

        foreach( $array as $entry )
        {
            $tmp = array_merge($tmp,explode(',',$entry));
        }

        foreach($tmp as $value) {
            if( substr($value,0,2) == 'CN' ){
                $groups[] = substr($value,3);
            }
        }

        return $groups;
    }
}
?>

Edit the variables at the tom of the file to reflect your setup.

Now we are going to make our Users->login function check the POST username and password against LDAP, and then if valid it will preform the login magic that auth used to do.

Fill your login function with this:

function login() {  
    App::import('Lib', 'ldap');
    if ($this->Session->read('Auth.User')) {
         $this->redirect(array('controller' => 'allocations', 'action' => 'index'));
    } elseif (!empty($this->data)) {
        $ldap = new ldap;
        if ($ldap->auth($this->Auth->data['User']['user'], $this->Auth->data['User']['password'])) {

            $userrow = $this->User->findByUsername($this->data['User']['user']);
            if (!$userrow) {
                $ldap_info = $ldap->getInfo($this->data['User']['user']);
                $this->data['User']['username'] = $this->data['User']['user'];
                $this->data['User']['name'] = $ldap_info['name'];
                $this->data['User']['group_id'] = 3; //sets the default group
                $this->add();
                $userrow = $this->User->findByUsername($this->data['User']['user']);
            }

            $user = $userrow['User'];

            $this->Auth->Session->write($this->Auth->sessionKey, $user);
            $this->Auth->_loggedIn = true;

            $this->Session->setFlash('You are logged in!');
            $this->redirect(array('controller' => 'allocations', 'action' => 'index'));
        } else {
            $this->Session->setFlash(__('Login Failed', true));
        }
    }
}

To quickly summarize, it first checks to see if the user is logged in, if not, and post data is provided it will check the provided credentials with the LDAP Lib. If valid it then attempts to get the user from the users table, if the user does not exist it creates it with information provided from LDAP and defaults set in the function.

I built this to still work with a users table to allow relationships between other models and users. but it can be used without a users table, just remove the if(!$userrow) statement and the line before it.

NOTE: If you use a users table, you do not need, and should not have, a password column.

That should be it! You should now be using LDAP for user credential validation.