Tuesday 7 June 2011

CakePHP: Authenticating against Microsoft Active Directory

My work environment uses Windows. Everyone logs on to an Active Directory based domain and this is how security is managed. When you have web applications running on Linux and you need some sort of authentication service, then you can either implement your own user data store or you can use an exiting. The good news is that MS AD is actually an LDAPv3 server, so getting data to and from LDAP is not that difficult in the world of both PHP and Cake.

The following "howto" will list the steps I followed in order to perform authentication from the company's Windows 2003 servers. I will also explain one approach for deciding how to grant users admin privileges on a CakePHP application based on membership of a Windows user group. Many might argue that I am oversimplifying, but I believe that on most occasions, this will be more than adequate.

In order to get started, remember that building an authentication system on a CakePHP application starts with creating a User model. Only difference will be here that the model in question will not get its data from an ordinary database table, but rather from an LDAP data source.

Get the LDAP datasource component

So this brings as to the first issue, which is: Get a datasource to read LDAP data. Fortunately the bakery has just what we need. The actual article that contains the code I am still using is this.

Analogrithems, however, the author of the datasource code, is providing new updates on github accessible via this link. The truth is that I have yet to test the new version, so if you are to follow the tutorial better stick with the one in the bakery.

No matter the version you choose you will end up with a file named ldap_source.php, that you will need to place in your APP/models/datasources/ directory.

Set up a connection using the new datasource

The next thing that needs to be done is to create a new connection that will use the new datasource. Open APP/config/database.php and add a new entry looking more or less like this

    var $ldap = array (
        'datasource' => 'ldap',
         // list of active directory hosts
        'host' => array(
                    'ldap1.example.com',
                    'ldap2.example.com',
                    'ldap3.example.com'
                ),
        'port' => 389,
         // location (root) in LDAP tree to start searching 
        'basedn' => 'DC=example,DC=com',
         // user to authenticate on AD server with
        'login' => 'cn=LDAPBindUser,cn=Users,dc=example,dc=com',
        'password' => 'abABa@332',
        'database' => '',
        'tls'      => false,
        'version'  => 3
    );

Verify with your network administrator that LDAPBindUser in created on the correct OU and that the base DN is correct, according to where you placed the user account. (Needless to say that the user must have next to no privileges at all)

Set up the User model class

The next step is our user model. This should be like any model, you have except that it will be based on the $ldap database config, its primary key will be the LDAP dn attribute and that it will need no table.

class User extends AppModel {
    var $name = 'User';
    var $useDbConfig = 'ldap';

    var $primaryKey = 'dn';
    var $useTable = '';

    /**
     * return true if the userName is a member of the groupName
     * Active Directory group
     */
    function isMemberOf($userName, $groupName)
    {
        // trivial check for valid names
        if (empty($userName) || empty($groupName))
            return false;

        // locate the user record
        $userData = $this->find('first',
                                array(
                                    'conditions' => array(
                                        'samaccountname' => $userName
                                    )
                                )
                            );
        // no user by that name exists
        if (empty($userData))
            return false;

        // check if the userin question belongs to any groups
        if (!isset($userData['User']['memberof']))
            return false;

        // search all groups that our user if a meber
        $groups = $userData['User']['memberof'];
        foreach( $groups as $index => $group)
            if (strpos( $group, $groupName) != false)
                return true;

        return false;
    }
}

We have also added a member function which allows us to determine if a user-name belongs to an Active directory group that will be very handy when checking for application privileges later on.

Set up the LdapAuth Component

And finally the "auth" part. The standard CakePHP Auth component will not work for us so we need a specialized version that thanks to analogthemes again is accessible from his web site through here. Just place ldap_auth.php to your APP/controllers/components/ directory and change your AppController so that it uses LdapAuth.

This however is not enough. There is one little thing that needs to be changed in the LdapAuth code, so that authenticating in AD works: Locate the login() function in line 153 of ldao_auth.php and change line 156 so that it looks like this:

     ...

     function login($uid, $password) {
        $this->__setDefaults();
        $this->_loggedIn = false;
        // $dn = $this->getDn('uid', $uid);
        $dn = $this->getDn('samaccountname', $uid);
        $loginResult = $this->ldapauth($dn, $password); 
        ...

My simple approach to security says that an application has two types of users. Readers (i.e. everyone) and writers (admins only). We set the administrative users to be the members of a specific group in the active directory server. That way our application controller looks like this :

class AppController extends Controller {
    const ADMIN_KEY  = 'myApp-Admin';
    const AUTH_GROUP = 'MyApp-Admins';

    var $components = array('RequestHandler', 'LdapAuth', 'Session');
    var $helpers = array(
                        'Form', 'Html', 'Locale', 'Session',
                        'Js' => array('Jquery')
                    );
    var $isAdmin;

    function beforeFilter()
    {
        // pr($this->referer());
        $this->isAdmin = false;
        $this->LdapAuth->authorize = 'controller';
        $this->LdapAuth->allowedActions = array(
                                            'index',
                                            'view',
                                            'details'
                                        );

        $this->LdapAuth->loginError = "Invalid credentials";
        $this->LdapAuth->authError  = "Not authorized!";

        $userInfo = $this->LdapAuth->user();

        if (!empty($userInfo)) {
            $user = $userInfo['User'];
            // setup display username and user's full name
            $this->set('loggedInUser', $user['samaccountname']);
            $this->set('loggedInFullName', $user['cn']);

            // if we already have the admin role stored in the session
            if ($this->Session->check(self::ADMIN_KEY))
                $this->isAdmin = $this->Session->read(self::ADMIN_KEY);
            else {
                // determine the admin role from wheither the current 
                // user is a member of the AUTH group
                $this->LoadModel('User');
                $this->isAdmin = $this->User->isMemberOf(
                                                $user['samaccountname'],
                                                self::AUTH_GROUP);
                $this->Session->write(self::ADMIN_KEY, $this->isAdmin);
            }
        } else {
            $this->Session->delete(self::ADMIN_KEY);
        }

        $this->set('admin', $this->isAdmin);
    }

    function isAuthorized()
    {
        // Only administrators have access to the CUD actions
        if ($this->action == 'delete' ||
            $this->action == 'edit' ||
            $this->action == 'add')
          return $this->isAdmin;

        return true;                
    }
}

That way, everyone can access our view, details and index actions of all the application controllers, but will need to enter the user name and password of an ADMIN_GROUP member to gain access to the rest of our controller methods. This tutorial however is has gone long enough. To finish it, please remember to create your user controller and you login user view ...

2 comments :

JB said...

Hi there,

Thank you very much for your contribution. However, I'm trying to use it with CAkephp 2.13 and it claims that user function should be at LdapAuth (as you are calling it at the beforeFilter function in the AppController.php
...
$userInfo = $this->LdapAuth->user();
...
but it is not there.

Do you have any suggestion ?

Thank you again.

JB

Athanassios Bakalidis said...

Hi JB
LdapAuth extends the Auth component. The user() function comes straight from the base class.

Are you sure that you 've got your includes and extends clauses correctly?