paolo@bimodesign.com | +34 608 61 64 10

Framework

        

Zend 2 - Kerberos and Active Directory

From wikipedia: "Active Directory is a directory service that Microsoft developed for Windows domain networks. It authenticates and authorizes all users and computers in a Windows domain type network. Active Directory makes use of Lightweight Directory Access Protocol (LDAP) versions 2 and 3, Microsoft's version of Kerberos, and DNS."
and "Kerberos is a computer network authentication protocol which works on the basis of 'tickets' to allow nodes communicating over a non-secure network to prove their identity to one another in a secure manner."
In this post I'll show you how implement Kerberos + AD and only AD in Zend 2 Framework.

Kerberos
The first step will be generate a keytab and configurign the virtual host in this way

...
    ## Kerberos Authentication
    AuthType Kerberos
    AuthName "My Zend Framework Application"
    KrbMethodNegotiate On
    KrbMethodK5Passwd Off
    Krb5KeyTab /etc/httpd/httpd.keytab
    Require valid-user
...

Note: For more details about the virtual host configuration, you'll ask to your sistem administrator.
Well...now we can modify the code. In my application I want to check the authentication for all the page of my site and in case of the user is not authenticated by Kerberos then he will be redirected to the login page where he must type your AD login and password.
Moreover, if the user's credentials are correct, then I'll get the name, surname, email and the other personal information that will return from the AD user data.
I modified the code of:
- /config/module.config.php
- /module/Module.php
In the module.config.php I inserted this code and configuration

// Configuration
return array(
    'module_config' => array(
        ...
        'KerberosLogin' => 'logintest',
        'KerberosPassword' => 'passwordtest',
        'KerberosLdapDn' => 'DC=es,DC=TEST,DC=net',
        'KerberosLdapConnect' => 'wldap.TEST.net',
        ...
    ),
...
// Routing to the Login form and Ldap authentication
return array(
    'router' => array(
        'routes' => array(
            'login' => array(
                'type' => 'segment',
                'options' => array(
                    'route'    => '/login/',
                    'defaults' => array(
                        'controller' => 'Login\Controller\Index',
                        'action'     => 'index',
                    ),
                ),
                'may_terminate' => true,
            ),
            'ldap' => array(
                'type' => 'segment',
                'options' => array(
                    'route'    => '/ldap',
                    'defaults' => array(
                        'controller' => 'Login\Controller\Index',
                        'action'     => 'ldap',
                    ),
                ),
                'may_terminate' => true,
            ),
        ),        
    ),
...
So, in the module.php I invoke the function for Kerberos in onBoostrap function
public function onBootstrap(MvcEvent $e) {
....
$this->getSessionFromKerberos();
...
}

and the getSessionFromKerberos, will have this code

public function getSessionFromKerberos() {
    $ldapconn = ldap_connect($this->moduleConfig['module_config']['KerberosLdapConnect'], "3268");
    ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
    ldap_set_option($ldapconn, LDAP_OPT_REFERRALS, 0);

    $username = $this->moduleConfig['module_config']['KerberosLogin'];
    $password = $this->moduleConfig['module_config']['KerberosPassword'];

    $ldap_dn = $this->moduleConfig['module_config']['KerberosLdapDn'];
    $bind__res = NULL;

    $bind__res = ldap_bind($ldapconn, $username, $password);

    $user_principal = @$_SERVER['REMOTE_USER'] ? $_SERVER['REMOTE_USER'] : @$_SERVER['REDIRECT_REMOTE_USER'];

    if ($user_principal !== null) {
        $filter = "(samaccountname=" . $user_principal . ")";
        $resultName = ldap_search($ldapconn, $ldap_dn, $filter);
        $entriesName = ldap_get_entries($ldapconn, $resultName);

        // Name and Surname
        $givenname = $entriesName[0]['displayname'];
        $nameSurname = $givenname[0];

        // Email
        $givenemail = $entriesName[0]['mail'];
        $email = $givenemail[0];

        // Company
        $givecompany = $entriesName[0]['company'];
        $empresa = $givecompany[0];

        // Save to session         

        $kerberosData = new Container('kerberosdata');
        $kerberosData->offsetSet('namesurname', $nameSurname);
        $kerberosData->offsetSet('email', $email);
        $kerberosData->offsetSet('empresa', $empresa);
    } else {
        //Redirect to login module
        $this->redirect()->toUrl('/login');
    }
}

In case of the user is not authenticated by Kerberos or if you decide to use ONLY the AD authentication (so you don't neet configure virtual host), in the login module/form, the code should be like this, where the form login action will redirect to /ldap action, of course.
Note: I added the check user Group AD control in my module in order to authenticate the user only if he belongs to one or more certain AD groups (for example, ASD-GROUPONE-ES and ASD-GROUPONE-INT).
use Zend\Ldap\Ldap;

public function ldapAction() {
    try {
        $form = new LoginForm();
        $request = $this->getRequest();

        if ($request->isPost()) {
            $login = new Login();
            $login->exchangeArray($request->getPost());

            if (empty($login->username) ||
                    empty($login->password)) {
                throw new \Exception("Error generico");
            }

            $ldapconn = ldap_connect($this->moduleConfig['module_config']['KerberosLdapConnect'], "3268");
            ldap_set_option($ldapconn, LDAP_OPT_PROTOCOL_VERSION, 3);
            ldap_set_option($ldapconn, LDAP_OPT_REFERRALS, 0);

            $username = $this->moduleConfig['module_config']['KerberosLogin'];
            $password = $this->moduleConfig['module_config']['KerberosPassword'];

            $ldap_dn = $this->moduleConfig['module_config']['KerberosLdapDn'];
            $bind__res = NULL;

            try {
                $bind__res = ldap_bind($ldapconn, $username, $password);
            } catch (\Exception $e) {
                $flagErrorLogin = "1";
                $session->offsetSet('errorlogin', $flagErrorLogin);

                die("Excepcion");
            }

            // To get the Name and Surname	
            $filter = "(samaccountname=" . $login->username . ")";
            $resultName = ldap_search($ldapconn, $ldap_dn, $filter);
            $entriesName = ldap_get_entries($ldapconn, $resultName);

            $givenname = $entriesName[0]['displayname'];

            // To get the group list of user
            $filter = "(samaccountname=" . $login->username . ")";
            $attrs = array("memberOf");

            $result = ldap_search($ldapconn, $ldap_dn, $filter, $attrs);
            $entries = ldap_get_entries($ldapconn, $result);
            array_shift($entries);

            // Simple array PHP to intersect with entries array
            $arrayGruposPermitidos = array("ASD-GROUPONE-ES",
                "ASD-GROUPONE-INT");

            $indGrps = 0;
            $grpsArray = array();

            foreach ($entries[0]['memberof'] as $grps) {
                if ($indGrps) {
                    $grps = substr($grps, 3);
                    $grps = substr($grps, 0, strpos($grps, ","));

                    $grpsArray[] = $grps;
                }
                $indGrps++;
            }
            $result = count(array_intersect($grpsArray, $arrayGruposPermitidos));

            if (!$result) {
                throw new \Exception("generic error");
            }

            $nameSurname = $givenname[0];

            // Save to session         
            $kerberosData = new Container('kerberosdata');
            // blablabla

            return $this->redirect()->toUrl('guia/');
        } else {
            throw new \Exception("Error generico");
        }
    } catch (\Exception $e) {
        $session = new Container('base');
        $flagErrorLogin = "1";
        $session->offsetSet('errorlogin', $flagErrorLogin);
        return $this->redirect()->toUrl('/');
    }
}

Note: this is the array output returned from AD user data

Array
(
    [count] => 1
    [0] => Array
        (
            [objectclass] => Array
                (
                    [count] => 4
                    [0] => top
                    [1] => person
                    [2] => organizationalPerson
                    [3] => user
                )
            [0] => objectclass
            [cn] => Array
                (
                    [count] => 1
                    [0] => USERNAME01
                )
            [1] => cn
            [sn] => Array
                (
                    [count] => 1
                    [0] => SurnameUser1 SurnameUser2
                )
            [2] => sn
            [l] => Array
                (
                    [count] => 1
                    [0] => Madrid
                )
            [3] => l
            [st] => Array
                (
                    [count] => 1
                    [0] => Madrid
                )
            [4] => st
            [title] => Array
                (
                    [count] => 1
                    [0] => Contrata
                )
            [5] => title
            [description] => Array
                (
                    [count] => 1
                    [0] => Contrata
                )
            [6] => description
            [postalcode] => Array
                (
                    [count] => 1
                    [0] => 28023
                )
            [7] => postalcode
            [physicaldeliveryofficename] => Array
                (
                    [count] => 1
                    [0] => Madrid - Av. Menendez Pelayo, 100-106
                )
            [8] => physicaldeliveryofficename
            [telephonenumber] => Array
                (
                    [count] => 1
                    [0] => 915999999
                )
            [9] => telephonenumber
            [givenname] => Array
                (
                    [count] => 1
                    [0] => NameUser
                )
            [10] => givenname
            [distinguishedname] => Array
                (
                    [count] => 1
                    [0] => CN=USERNAME,OU=Usuarios,OU=DGTP,OU=Areas Comunes,DC=es,DC=OrganizationName,DC=net
                )
            [11] => distinguishedname
            [instancetype] => Array
                (
                    [count] => 1
                    [0] => 0
                )
            [12] => instancetype
            [whencreated] => Array
                (
                    [count] => 1
                    [0] => 20131002080457.0Z
                )
            [13] => whencreated
            [whenchanged] => Array
                (
                    [count] => 1
                    [0] => 20140628071619.0Z
                )
            [14] => whenchanged
            [displayname] => Array
                (
                    [count] => 1
                    [0] => SurnameUser1 SurnameUser2, NameUser
                )
            [15] => displayname
            [usncreated] => Array
                (
                    [count] => 1
                    [0] => 12498072
                )
            [16] => usncreated
            [memberof] => Array
                (
                    [count] => 7
                    [0] => CN=GCOL_ACTP_EXCP_DLP,OU=Colectivos,OU=GruposBase,OU=AUMP,OU=SSCC,DC=es,DC=OrganizationName,DC=net
                    [1] => CN=GSEC_SVC_POLSEC_EXC_PROTECTORCORPORATIVO,OU=Excepciones,OU=Securizacion,OU=Perfiles,OU=GruposBase,OU=AUMP,OU=SSCC,DC=es,DC=OrganizationName,DC=net
                    [2] => CN=GSEC-USUARIOS-PROXY2,OU=Excepciones,OU=Securizacion,OU=Perfiles,OU=GruposBase,OU=AUMP,OU=SSCC,DC=es,DC=OrganizationName,DC=net
                    [3] => CN=GDGTP-MigracionMSXCol,OU=Grupos,OU=DGTP,OU=Areas Comunes,DC=es,DC=OrganizationName,DC=net
                    [4] => CN=GCOL_TEMP2,OU=TEMP,OU=GruposBase,OU=AUMP,OU=SSCC,DC=es,DC=OrganizationName,DC=net
                    [5] => CN=GSEC_SVC_POLVPN_EXC_CLARITY_COLABORADORES,OU=Excepciones,OU=VPN-SSL,OU=Perfiles,OU=GruposBase,OU=AUMP,OU=SSCC,DC=es,DC=OrganizationName,DC=net
                    [6] => CN=DGTP-BLOG-CCI,OU=BLOG CCI,OU=DGTP,OU=Colectivos,OU=GruposBase,OU=AUMP,OU=SSCC,DC=es,DC=OrganizationName,DC=net
                )
            [17] => memberof
            [usnchanged] => Array
                (
                    [count] => 1
                    [0] => 52311605
                )
            [18] => usnchanged
            [co] => Array
                (
                    [count] => 1
                    [0] => España
                )
            [19] => co
            [delivcontlength] => Array
                (
                    [count] => 1
                    [0] => 10240
                )
            [20] => delivcontlength
            [department] => Array
                (
                    [count] => 1
                    [0] => Desarrollo Web y Consultoria
                )
            [21] => department
            [company] => Array
                (
                    [count] => 1
                    [0] => COMPANY NAME, S.R.L.
                )
            [22] => company
            [homemta] => Array
                (
                    [count] => 1
                    [0] => CN=Microsoft MTA,CN=SMNMSX900-006,CN=Servers,CN=Exchange Administrative Group (FYDIBOHF23SPDLT),CN=Administrative Groups,CN=COMPANY,CN=Microsoft Exchange,CN=Services,CN=Configuration,DC=OrganizationName,DC=net
                )
            [23] => homemta

...etc...