PHP Active Directory LDAP Authentication

Posted on 03/05/2012 · Posted in Development

While searching online there is a lot of information regarding Microsoft Active Directory (AD) connecting and binding. Binding with Active Directory is one of the key components required when creating applications for Enterprise porpoises, and is a little bit harder to do properly than what online guides give away. In this article I’m going to go trough some of the concepts we had to work with regarding AD integration and hopefully it will be useful to others as well. Examples here are in PHP but easily transferred to other languages as well. Do not that the exmaples in this post are not complete and should be used as a guideline instead for creating your own LDAP authentication and actually understanding it!

When authenticating a user with Active Directory binding should be done with the user account itself, some guides recommend using a service account but this way the username and password can’t be verified, only that the user account exists in the database. This might be obvious, but still worth mentioning. A service account is only useful when creating import sequences or crawling the AD identity store. To access user information in Active Directory a 4 step procedure is made.

  1. Connect to Active Directory
  2. Bind with Active Directory
  3. Search for the user
  4. Get the entries

Simple procedure, ey?

At first lets take a look at usernames in AD. Those who have been working with AD before know that users can enter their login in three different ways; username (normal single domain way of doing it), username@domain.com (userPrincipalname) and domainusername (pre-Windows2000 format). All these variatins should be considered when creating AD login and thus a simple check is required. If you are working with a single domain  environment, this isn’t required but as soon as you work with a multi-domain environment it becomes something you need to consider.

/**
* Prepare the username
**/
//Check if user enterd username in UPN format
if(strpos($user_name, '@', 1))
{
   $parts = explode("@",$user_name);
   $user_name = $parts[0];
   $user_domain = $parts[1];
}
//Check if user entered username in pre-2000 format
else if(strpos($user_name, ''))
{
   $parts = explode('', $user_name);
   $user_name = $parts[1];
   $user_domain = $parts[0].".".$this->_config->default_domain;
}
// Else return default
else $user_domain = $this->_config->default_domain;

In this example i’m using the default domain of example.local and anyone who belongs to a sub domain needs to type in their domain in the username. So if the user is in the extra subdomain (extra.example.local), they would need to type in either extrausername or username@extra.example.local. This also makes it a little bit easier to track down the problem if a user experiences log-in issues as you can easily cut down the alternatives if you know what domain the user belongs to.

Once we have the username in a desired format we need to connect to the correct AD server. If you want to bind to a Global Catalog (GC) server you will need to change the port to 3268, while a normal LDAP connect requires port 389.

To connect to the server the following is required (Keeping it simple):

$connect = ldap_connect($ip, $port);

You can add complexity to this by creating a loop that goes trough a list of domain controllers and stops when a connection is made. Just remember that you can not bind to a server from a different domain, unless it’s a global catalog server…

/**
* Loop trough the result and stop when a connection is made
**/
$connect = FALSE;
while (!$connect && list(, $server) = each($ldap_servers)) {
   /**
   * Define server ip and port from array
   * and try to connect to LDAP server
   **/
   $connect = ldap_connect($ip, $port);
}
if (!($connect))
{
   if($this->_config->login_fallback === TRUE)
   {
      //Do a local authentication
   }
   else
   {
      //Give an error message that could not connect
   }
}

After a successfull connect we need to authenticate the username and password (Example is in UPN format only!). Note that the username in Pre-Windows 2000 format might not the be the same as the UPN format username! LDAP bind is done as following:

$ldapUser = "$user_name@$user_domain";
/**
* Bind to LDAP using entered username and password
**/
if (!($bind = ldap_bind($connect, $ldapUser, $passwd))) {
   //Give an error message that the username and/or password is incorrect
}
/**
* If we got a result, search for the username in the haystack (OU Structure)
**/
else {
   /**
   * Define what we want from LDAP
   **/
   $filter="userprincipalname=$user_name@$user_domain";
   $entries= array("ou", "sn", "givenname", "mail", "userprincipalname", "objectsid", "objectguid");
   /**
   * Search for the user in the LDAP directory
   **/
   $result = ldap_search($connect, $haystack, "(&(objectClass=user)($filter))", $entries);
   $info = ldap_get_entries($connect, $result);
}

Notice that in the else clause we search fot the user usign ldap_search and after that we get the entries of the result. This is to ensure that the user who authenticates is truly allowed to the system. The $haystack is not defined in this example but it needs to the OU structure where the username resides. So it could be as following:

$haystack = “OU=allowed,OU=users,DC=example,DC=local”;

Remember that any user and organizational unit under “Allowed” does yield a search result, but if a user is in OU=admins,OU=users,DC=Example,DC=local it does not give a search result. $entries is defined so that we only get the results we truly want and not all the attributes found for a user (which is a lot). This makes things a little bit faster :) Now that we’ve done a successfull connect, bind, search and gotten the entries we want. We can close the AD connection with the following line:

ldap_unbind($connect);

Note thatbinding is done with the UPN of the user, this is worth gold when debugging user login problems as the UPN might be different in every organization out there.

With the information retrieved we can now start creating a local data store that mimics the information found in Active Directory. We now know the users firname, lastname, username, e-mail, SID and GUID. You might be asking why we need the SID (Seucirty Identifier) and GUID (Global Unique Identifier) ? Well we don’t exactly need both of them, GUID would be enough. The reason behind this is simple, GUID is the ONLY TRULY unique and static attribute of a user in AD. Username and e-Mail are unique, but they can be changed. Some administrators re-user username in Ad to shorten the time required to create a user incase an employee has been fired and someone is taking over his job. This way an administrator does not have to trace down what access is required as the username can be just given to the new employee. This saves time but does pose a problem when creating AD integration. What if the username is changed of a user that already exists in the database? To get rid of this problem we can use the GUID (or SID for that matter) of a user. The trick here is to handle the GUID and SID properly as they are stored as binary in AD.

SID (Security Identifier)

Scott2500UK created a simple SID conversion so that it’s useable and storable in a DB in a readable format: here

Here is the code in a usable function:

public static function SIDtoString($ADsid)
{
   $sid = "S-";
   //$ADguid = $info[0]['objectguid'][0];
   $sidinhex = str_split(bin2hex($ADsid), 2);
   // Byte 0 = Revision Level
   $sid = $sid.hexdec($sidinhex[0])."-";
   // Byte 1-7 = 48 Bit Authority
   $sid = $sid.hexdec($sidinhex[6].$sidinhex[5].$sidinhex[4].$sidinhex[3].$sidinhex[2].$sidinhex[1]);
   // Byte 8 count of sub authorities - Get number of sub-authorities
   $subauths = hexdec($sidinhex[7]);
   //Loop through Sub Authorities
   for($i = 0; $i < $subauths; $i++) {
      $start = 8 + (4 * $i);
      // X amount of 32Bit (4 Byte) Sub Authorities
      $sid = $sid."-".hexdec($sidinhex[$start+3].$sidinhex[$start+2].$sidinhex[$start+1].$sidinhex[$start]);
   }
   return $sid;
}

GUID (Global Unique Identifier)

Global Unique Identifiers are a little bit different than SID. Although extremely remote, there is a slight possibility that there are two same SID’s in a forest. But the main reason not to use SID’s is that they are regenerated in certain occasions. Thus using a GUID is a better alternative as it is always unique and it does not change inside a forest. The guid does require some work to make it in a readable format.

From Binary Octet string to GUID string (using the octet string in the message above):

  1. Each pair or characters is called an octet. Work with the GUID from left to right.
  2. Take the first 4 octets and reverse their order: 01234567 –> 67452301
  3. Take the next 2 octets and reverse their order: 89AB –> AB89
  4. Repeat for the next 2 octets: CDEF –> EFCD
  5. The rest of the octet string is identical for both formats so just add a hyphen: ABCDEFABCDEFABCD –> ABCD-EFABCDEFABCD

Here is a usable code for generating a readable guid:

public static function GUIDtoString($ADguid)
{
   $guidinhex = str_split(bin2hex($ADguid), 2);
   $guid = "";
   //Take the first 4 octets and reverse their order
   $first = array_reverse(array_slice($guidinhex, 0, 4));
   foreach($first as $value)
   {
      $guid .= $value;
   }
   $guid .= "-";
   // Take the next two octets and reverse their order
   $second = array_reverse(array_slice($guidinhex, 4, 2, true), true);
   foreach($second as $value)
   {
      $guid .= $value;
   }
   $guid .= "-";
   // Repeat for the next two
   $third = array_reverse(array_slice($guidinhex, 6, 2, true), true);
   foreach($third as $value)
   {
      $guid .= $value;
   }
   $guid .= "-";
   // Take the next two but do not reverse
   $fourth = array_slice($guidinhex, 8, 2, true);
   foreach($fourth as $value)
   {
      $guid .= $value;
   }
   $guid .= "-";
   //Take the last part
   $last = array_slice($guidinhex, 10, 16, true);
   foreach($last as $value)
   {
      $guid .= $value;
   }
   return $guid;
}

Hope you found this information useful! :]