Active Directory User Validation Through LDAP and PHP

We were recently tasked with the redesign of a fairly large intranet application which used Microsoft Active Directory for user authentication. The previous version of the site was created with ASP/VBScript and so our first challenge was duplicating (or in this case, improving) the LDAP connection system using PHP.

General Concepts

First of all, there are several methods of validating and maintaining session state across website pages and in this case I chose to use the PHP $_SESSION variable. Depending on your situation, that may or may not be the best bet -- there are limitations to using $_SESSION exclusively, notably the fact that the session won't persist across multiple servers in a load-balanced environment. In my case this site was to be set up on a single server and it is accessible only to computers on the local network or VPN, so $_SESSION works rather well.

Second, I used a variety of Active Directory fields for identification and administration access management. These fields are the ones that I will be referencing in the code samples below. Keep in mind that your mileage may vary depending on what data you want to display and use throughout your project.

And finally, the LDAP authentication syntax is a bit different in PHP than it was in ASP/VBScript, but in both cases I've saved the connection values as constants which are accessible to all files:

<?php
// LDAP Constants
define("LDAP_SERVER","localhost");
define("LDAP_ADMIN_USER","username@domain.local");
define("LDAP_ADMIN_PASS","password");
define("LDAP_DOMAIN","YOUR_DOMAIN");
define("LDAP_BASE_DN","OU=Top Organizational Unit,DC=domain,DC=local");
?>

The LDAP Validation Function

For simplicity, I created a function that is called on every page that requires authentication (all of them, in my case.) Here is the function:

<?php
// LDAP Security Validation
// $admin_req = level of administrative access required (dept admin, hr, it, etc.)
function validateUser($admin_req = ''){
  // Check for existing valid session
  session _ start();
  if(!isset($_SESSION['id']) || (isset($_SESSION['id']) && (time() - $_SESSION['modified']) > 3600)){
	// Session not found, perform LDAP lookup
	// Identify the username from the AUTH_USER variable
	$domain_pos = strlen(LDAP_DOMAIN) + 1;
	$current_user = substr($_SERVER['AUTH_USER'],$domain_pos);

	$conn = ldap_connect(LDAP_SERVER);
	ldap_set_option($conn, LDAP_OPT_PROTOCOL_VERSION,3);
	ldap_set_option($conn, LDAP_OPT_REFERRALS, 0);
	$bind = ldap_bind($conn,LDAP_ADMIN_USER,LDAP_ADMIN_PASS);
	$filter = "(samaccountname=" . $current_user .")";
	$attr = array('displayname','postalcode','memberOf','physicaldeliveryofficename','mail','manager');
	$result = ldap_search($conn,LDAP_BASE_DN,$filter,$attr);
	
	// Save values to the session array
	if($result){
	  $entries = ldap_get_entries($conn,$result);
	  $_SESSION['id'] = uniqid();
	  $_SESSION['username'] = $entries['0']['displayname']['0'];
	  $_SESSION['zip'] = $entries['0']['postalcode']['0'];
	  if(isset($entries['0']['mail']['0'])){
	    $_SESSION['email'] = $entries['0']['mail']['0'];
	  }else{
		$_SESSION['email'] = 'memail@mycompany.com';
	  }
	  $_SESSION['branch'] = $entries['0']['physicaldeliveryofficename']['0'];
	  $_SESSION['modified'] = time();
	  
	  // Separate manager from string
	  if(isset($entries['0']['manager']['0'])){
	    $arrmgr = explode(",",$entries['0']['manager']['0']);
		$_SESSION['manager'] = str_replace('CN=','',$arrmgr[0]);
	  }else{
		$_SESSION['manager'] = 'Not Defined';
	  }
	  // Find any groups that the user belongs to
	  if(isset($entries['0']['memberof'])){
	    foreach($entries['0']['memberof'] as $group){
		  switch($group){
		    case 'CN=IT Admins,CN=Users,DC=domain,DC=local':
			  $_SESSION['itadmin'] = 1;
			  break;
			case 'CN=Department Admins,CN=Users,DC=domain,DC=local':
			  $_SESSION['deptadmin'] = 1;
			  break;
			case 'CN=HR Admins,CN=Users,DC=domain,DC=local':
			  $_SESSION['hradmin'] = 1;
			  break;
		  }
		}
	  }
	}else{
      header("Locaton: /error.asp?e=1");
	  exit();
	}
	ldap_unbind($conn);
  }
  // Confirm access rights to admin pages
  switch($admin_req){
    case 'dept':
	  if(!isset($_SESSION['deptadmin'])){
	    header('Location: /error.php?e=2');
	  }
	  break;
	case 'hr':
	  if(!isset($_SESSION['hradmin'])){
	    header('Location: /error.php?e=2');
	  }
	  break;
	case 'it':
	  if(!isset($_SESSION['itadmin'])){
	    header('Location: /error.php?e=2');
	  }
	  break;
	case 'any':
	  if(!isset($_SESSION['itadmin']) && !isset($_SESSION['deptadmin']) && !isset($_SESSION['hradmin'])){
		  header('Location: /error.php?e=2');
		}
		break;
	case 'all':
	  if(!isset($_SESSION['itadmin']) || !isset($_SESSION['deptadmin']) || !isset($_SESSION['hradmin'])){
	    header('Location: /error.php?e=2');
	  }
	  break;
	case '':
	  break;
  }
}
?>

The function actually operates in two phases -- if the user doesn't have a valid session established, it checks their logged in username $_SERVER['AUTH_USER'] against LDAP and if found, retrieves a handful of key pieces of information.

$attr = array('displayname','postalcode','memberOf','physicaldeliveryofficename','mail','manager');

The above line is the array of fields that are saved from the LDAP query. Some of those are used for information -- postalcode is used in a Yahoo! Weather API implementation, for example -- but memberOf is used for authentication. Any content in the memberOf array is parsed and identified and additional session variables are specified to match.

The second phase of this function checks for proper administrative level, if the page calls for it. If the proper session value that corresponds to the value of $admin_req is missing, the user is redirected to an error page with an error code that describes the problem.

Calling the Function

Now that I've got the function written, implementing it on every page that requires authentication is fairly easy. Below are the code samples for regular, non-administrative pages and those that require some sort of administrative authentication. Pretty simple!

validateUser(); // non-admin
validateUser('dept'); // department admin
validateUser('hr'); // human resources admin

Hi there, just wanted to say,

Hi there, just wanted to say, I liked this post. It was practical. Keep on posting!

I am getting some error while

I am getting some error while cronigunifg openfire with active directory 2008. I put When I gave CN, DN, OU the same mentioned above I got this error. [LDAP: error code 8 - 00002028: LdapErr: DSID-0C0901FC, comment: The server requires binds to turn on integrity checking if SSL\TLS are not already active on the connection, data 0, v1db0]I am trying to figure out problem. Please Help Me.Thanks !

Post new comment

  • You can use BBCode tags in the text.

More information about formatting options