Yii Active Directory UserIdentity Authentication

Goal: Create a Login page that works with Active Directory using the Yii Framework

Prerequisites: Yii Framework, Basic understanding of Yii Configurations, Basic PHP Knowledge is helpful

Yii is a very powerful and extendable framework. In a many corporate environments where Active Directory is used as the primary means of authentication, it makes sense to integrate that same authentication form into your website. This helps to eliminate the need to maintain another username/password set, makes enforcing password policies a breeze, and makes life a whole lot easier in general.

The scripts below are relatively simple, and yet relatively robust for the small amount of time it takes to implement them. This method of integrating active directory with your Yii php instance supports restricting login to a specific organizational units or domains. It allows you to specify multiple OUs or Domains in case you want to allow users from one domain in OU Users to join and users from another domain. This PHP script also supports naming multiple domain controllers such that if one goes offline it will fail over to the next one in the list.

So lets get started implementing. The first thing we'll do is to create our UserIdentity file. This is how you do authentication logic for logging in with Yii. This file you can copy and paste and you should not need to make any changes to this file directly. This is where all of the LDAP stuff actually happens, so for those of you finding this just looking for a way to authenticate in PHP this file is probably the only one you really care about (and maybe a bit of the configuration file).

/protected/components/UserIdentity.php

<?php

/**
 * UserIdentity represents the data needed to identity a user.
 * It contains the authentication method that checks if the provided
 * data can identity the user.
 */
class UserIdentity extends CUserIdentity
{

    const ERROR_NO_DOMAIN_CONTROLLER_AVAILABLE = 1001; // could not bind anonymously to any domain controllers
    const ERROR_INVALID_CREDENTIALS = 1002; // could not bind with user's credentials
    const ERROR_NOT_PERMITTED = 1003; //user was not found in search criteria

    private $_options;

    private $_domain;
    private $_email;
    private $_firstName;
    private $_lastName;
    private $_securityGroups;

    private $_loginEmail = false;

    public function __construct($username = null,$password = null)
    {
        $this->_options = Yii::app()->params['ldap'];

        $this->username = $username;
        $this->password = $password;

        if(strpos($username,'@') !== false){
            $this->_loginEmail = $username;
            $exploded = explode('@',$username);
            $this->username = $exploded[0];
        }

        $slashPos = strpos($this->username, "\\");
        if($slashPos !== false){
            $this->username = substr($this->username, $slashPos+1);
            $this->_domain = substr($this->username, 0, $slashPos);
        }else{
            $this->_domain = $this->_options['defaultDomain'];
        }
    }

    public function authenticate()
    {
        $this->errorCode = self::ERROR_NONE;
        if($this->username != '' && $this->password != ''){

            $bind = false;
            $connected = false;
            $ldap = false;

            //connect to the first available domain controller in our list
            foreach($this->_options['servers'] AS $server){
                $ldap = ldap_connect($server);
                if($ldap !== false){
                    ldap_set_option($ldap, LDAP_OPT_PROTOCOL_VERSION, 3);
                    ldap_set_option($ldap, LDAP_OPT_REFERRALS, 0);
                    $connected = @ldap_bind($ldap); //test if we can connect to ldap using anonymous bind
                    if($connected){
                        if($this->_loginEmail === false){
                            $ldaprdn = $this->_domain . "\\" . $this->username;
                        }else{
                            $ldaprdn = $this->_loginEmail;
                        }
                        $bind = @ldap_bind($ldap, $ldaprdn, $this->password);
                        break; //we connected to one successfully
                    }
                }
            }

            //were we able to connect to any domain controller?
            if(!$connected){
                $this->errorCode = self::ERROR_NO_DOMAIN_CONTROLLER_AVAILABLE;
            }else{
                //were we able to authenticate to a domain controller as our user?
                if ($bind) {
                    //if we can bind to active directory we must have valid AD credentials
                    $filter='(sAMAccountName='.$this->username.')';

                    $conn=array();
                    for($i = 0; $i < count($this->_options['search']); $i++){
                        $conn[] = $ldap;
                    }
                    $results = ldap_search($conn,$this->_options['search'],$filter);
                    $foundInSearch = false;
                    foreach($results AS $result){
                        $info = ldap_get_entries($ldap, $result);
                        if($info['count'] > 0){
                            $this->_firstName = (isset($info['0']['givenname']['0']))?($info['0']['givenname']['0']):('');
                            $this->_lastName = (isset($info['0']['sn']['0']))?($info['0']['sn']['0']):('');
                            $this->_email = (isset($info['0']['mail']['0']))?($info['0']['mail']['0']):('');

                            $this->_securityGroups = array();
                            foreach($info['0']['memberof'] AS $sg){
                                preg_match('/CN=(.*?),/',$sg, $matches);
                                if(isset($matches[1])){
                                    $this->_securityGroups[] = $matches['1'];
                                }
                            }
                            sort($this->_securityGroups);

                            $foundInSearch = true;
                            break;
                        }
                    }

                    if(!$foundInSearch){
                        $this->errorCode = self::ERROR_NOT_PERMITTED;
                    }
                }else{
                    //if we can't bind to active directory it means that the username / password was invalid
                    $this->errorCode = self::ERROR_INVALID_CREDENTIALS;
                }
            }
        }else{
            //if username or password is blank don't even try to authenticate
            $this->errorCode = self::ERROR_INVALID_CREDENTIALS;
        }

        switch($this->errorCode){
            case self::ERROR_INVALID_CREDENTIALS :
                $this->errorMessage = 'Invalid Credentials.';
                break;
            case self::ERROR_NO_DOMAIN_CONTROLLER_AVAILABLE :
                $this->errorMessage = 'No domain controller available.';
                break;
            case self::ERROR_NOT_PERMITTED:
                $this->errorMessage = 'Not permitted in application.';
                break;
            case self::ERROR_NONE :
                $this->setState('firstName', $this->_firstName);
                $this->setState('lastName', $this->_lastName);
                $this->setState('email', $this->_email);
                $this->setState('adSecurityGroups', $this->_securityGroups);
                break;
            default : $this->errorMessage = 'Unable to Authenticate';
        }

        return !$this->errorCode;
    }

    public function getName(){
        return $this->_firstName.' '.$this->_lastName;
    }
}

Now we need to create the validation logic for the forms. In Yii we can do some pretty fancy stuff using the models so I'll use that here. Keep in mind though that you could just as easily leave out the model and build your form manually with the view files and put your logic in your controller.

/protected/models/LoginForm.php

<?php

/**
 * LoginForm class.
 * LoginForm is the data structure for keeping
 * user login form data. It is used by the 'login' action of 'SiteController'.
 */
class LoginForm extends CFormModel
{
    public $username;
    public $password;
    public $rememberMe;

    private $_identity;

    /**
     * Declares the validation rules.
     * The rules state that username and password are required,
     * and password needs to be authenticated.
     */
    public function rules()
    {
        return array(
            // username and password are required
            array('username, password', 'required'),
            // rememberMe needs to be a boolean
            array('rememberMe', 'boolean'),
            // password needs to be authenticated
            array('password', 'authenticate'),
        );
    }

    /**
     * Declares attribute labels.
     */
    public function attributeLabels()
    {
        return array(
            'rememberMe'=>'Remember me next time',
        );
    }

    /**
     * Authenticates the password.
     * This is the 'authenticate' validator as declared in rules().
     */
    public function authenticate($attribute,$params)
    {
        if(!$this->hasErrors())
        {
            $this->_identity=new UserIdentity($this->username,$this->password);
            if(!$this->_identity->authenticate())
                $this->addError('password',$this->_identity->errorMessage);
        }
    }

    /**
     * Logs in the user using the given username and password in the model.
     * @return boolean whether login is successful
     */
    public function login()
    {
        if($this->_identity===null)
        {
            $this->_identity=new UserIdentity($this->username,$this->password);
            $this->_identity->authenticate();
        }
        if($this->_identity->errorCode===UserIdentity::ERROR_NONE)
        {
            $duration=$this->rememberMe ? 3600*24*30 : 0; // 30 days
            Yii::app()->user->login($this->_identity,$duration);
            return true;
        }
        else
            return false;
    }
}

Next we'll set up our action for the login page. I put this in my site controller. Again it should just be a straight copy and paste, no edits necessary.

/protected/controllers/SiteController.php segment

    /**
     * Displays the login page
     */
    public function actionLogin()
    {
        $model=new LoginForm;

        // if it is ajax validation request
        if(isset($_POST['ajax']) && $_POST['ajax']==='login-form')
        {
            echo CActiveForm::validate($model);
            Yii::app()->end();
        }

        // collect user input data
        if(isset($_POST['LoginForm']))
        {
            $model->attributes=$_POST['LoginForm'];
            // validate user input and redirect to the previous page if valid
            if($model->validate() && $model->login())
                $this->redirect('/');
        }
        // display the login form
        $this->render('login',array('model'=>$model));
    }
Now we need to create the view file for our form. Again, no changes required here though you can style it to your own liking. When I set this up I used the default Yii Classic template.

/protected/views/site/login.php

<?php
/* @var $this SiteController */
/* @var $model LoginForm */
/* @var $form CActiveForm  */

$this->pageTitle=Yii::app()->name . ' - Login';
$this->breadcrumbs=array(
    'Login',
);
?>

<h1>Login</h1>

<p>Please fill out the following form with your login credentials:</p>

<div class="form">
<?php $form=$this->beginWidget('CActiveForm', array(
    'id'=>'login-form',
    'enableClientValidation'=>true,
    'clientOptions'=>array(
        'validateOnSubmit'=>true,
    ),
)); ?>

    <p class="note">Fields with <span class="required">*</span> are required.</p>

    <div class="row">
        <?php echo $form->labelEx($model,'username'); ?>
        <?php echo $form->textField($model,'username'); ?>
        <?php echo $form->error($model,'username'); ?>
    </div>

    <div class="row">
        <?php echo $form->labelEx($model,'password'); ?>
        <?php echo $form->passwordField($model,'password'); ?>
        <?php echo $form->error($model,'password'); ?>
        <p class="hint">
            Hint: You may login with <kbd>demo</kbd>/<kbd>demo</kbd> or <kbd>admin</kbd>/<kbd>admin</kbd>.
        </p>
    </div>

    <div class="row rememberMe">
        <?php echo $form->checkBox($model,'rememberMe'); ?>
        <?php echo $form->label($model,'rememberMe'); ?>
        <?php echo $form->error($model,'rememberMe'); ?>
    </div>

    <div class="row buttons">
        <?php echo CHtml::submitButton('Login'); ?>
    </div>

<?php $this->endWidget(); ?>
</div><!-- form -->

Finally, we get around to configuring our LDAP configurations. I have set up all the configuration options as part of a ldap parameter. You should add these parameters to your Yii configuration file and tweak as necessary. Here is an outline of what each config option is used for:

servers - a list of IP addresses or DNS names which you want to try and authenticate against. The first one in the list will always be tried first, it will only move on to the next one if it fails to bind to the ldap port.

defaultDomain - a default domain which you want to authenticate to if the user does not try logging in using an email address or using the DOMAIN\USERNAME format.

search - an array of LDAP filters which the user authenticating will be searched for in. If the user does not belong to any of the filters applied, they will receive an access denied message when they try to authenticate. In my example I am only including the Faculty and Staff OUs so if someone in the students OU tries logging in they will be denied access.

Sample Params in Config File: /protected/config/main.php segment

'params'=>array(        
        'ldap' => array (
            'servers' => array(
                'dc1.example.com',
                'dc2.example.com',
                '10.1.0.3',
            ),
            'defaultDomain' => 'EXAMPLE',
            'search'=>array(
                'ou=Students,dc=EXAMPLE,dc=net',
                'ou=Faculty,dc=EXAMPLE,dc=net',
            ),
        ),
),

For those of you who want a quick start I have attached a zip file containing the necessary files to make this work with a stock Yii setup. Just drop the files in, tweak the config, and you're ready to authenticate against your active directory servers!

Yii_Active_Directory_LDAP.zip

Loading Conversation