This topic is locked

HowTo use LDAP Authentication

7/15/2008 8:28:59 AM
PHPRunner Tips and Tricks
T
thesofa author

Hi,

Several times over the past years of using PHPR, I have had to write web pages needing a loin for a school network.

Every time, the teachers complain about having to learn a different password, but they will not tell me thir passwords so I can set up an authentication table.

Over a period of time , I have played with LDAP and I now present a working example.

Several steps are involved, these are

  1. set up php and mysql to use LDAP
  2. create a readonly ldap user for authentication
  3. get and install the LDAP class files
  4. alter the code generated by PHPR to use LDAP


First of all read all of this post here it details a working example from Version 3.0
Sadly this does not work, BUT the following code bits will allow you to hack your code to make it work, I hope

<?php

/*

PHP LDAP CLASS FOR MANIPULATING ACTIVE DIRECTORY

Version 1.5



Written by Scott Barnett

email: scott@wiggumworld.com

http://adldap.sourceforge.net/



Copyright (C) 2006 Scott Barnett



I'd appreciate any improvements or additions to be submitted back

to benefit the entire community :)



Works with both PHP 4 and PHP 5



The examples should be pretty self explanatory. If you require more

information, please visit http://adldap.sourceforge.net/





This library is free software; you can redistribute it and/or

modify it under the terms of the GNU Lesser General Public

License as published by the Free Software Foundation; either

version 2.1 of the License, or (at your option) any later version.



This library is distributed in the hope that it will be useful,

but WITHOUT ANY WARRANTY; without even the implied warranty of

MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU

Lesser General Public License for more details.
********************************************************************

Something to keep in mind is that Active Directory is a permissions

based directory. If you bind as a domain user, you can't fetch as

much information on other users as you could as a domain admin.

********************************************************************
FUNCTIONS:



authenticate($username,$password)

Authenticate to the directory with a specific username and password



user_info($user,$fields=NULL)

Returns an array of information for a specific user
user_groups($user,$recursive=NULL)

Returns an array of groups that a user is a member off
user_ingroup($user,$group,$recursive=NULL)

Returns true if the user is a member of the group



group_info($group)

Returns an array of information for a specific group



all_users($include_desc = false, $search = "*", $sorted = true)

Returns all AD users (expensive on resources)



all_groups($include_desc = false, $search = "*", $sorted = true)

Returns all AD groups (expensive on resources)
*/
// Different type of accounts in AD

define ('ADLDAP_NORMAL_ACCOUNT', 805306368);

define ('ADLDAP_WORKSTATION_TRUST', 805306369);

define ('ADLDAP_INTERDOMAIN_TRUST', 805306370);

define ('ADLDAP_SECURITY_GLOBAL_GROUP', 268435456);

define ('ADLDAP_DISTRIBUTION_GROUP', 268435457);

define ('ADLDAP_SECURITY_LOCAL_GROUP', 536870912);

define ('ADLDAP_DISTRIBUTION_LOCAL_GROUP', 536870913);
class adLDAP {

// BEFORE YOU ASK A QUESTION, PLEASE READ THE FAQ

// http://adldap.sourceforge.net/faq.php
// You will need to edit these variables to suit your installation

var $_account_suffix="@your.domain";

var $_base_dn ="DC=your,DC=domain";

//e.g. var $_account_suffix="@xlinesoft.com";

// var $_base_dn ="DC=xlinesoft,DC=com";

// note the syntax and spacing, LDAP is very fussy



// An array of domain controllers. Specify multiple controllers if you

// would like the class to balance the LDAP queries amongst multiple servers

var $_domain_controllers = array ("your-forest-root.your.domain");



// optional account with higher privileges for searching

var $_ad_username="ldap";

var $_ad_password="eckle67fecKIN98we11ythumP";



// AD does not return the primary group. http://support.microsoft.com/?kbid=321360

// This tweak will resolve the real primary group, but may be resource intensive.

// Setting to false will fudge "Domain Users" and is much faster. Keep in mind though that if

// someone's primary group is NOT domain users, this is obviously going to bollocks the results

var $_real_primarygroup=true;



// When querying group memberships, do it recursively

// eg. User Fred is a member of Group A, which is a member of Group B, which is a member of Group C

// user_ingroup("Fred","C") will returns true with this option turned on, false if turned off

var $_recursive_groups=true;



// You should not need to edit anything below this line

//********************************************************************************

**********

//DEAD RIGHT YOU DO NOT, LEAVE IT WELL ALONE!!!!!!!!!!!!!!!

//other variables

var $_user_dn;

var $_user_pass;

var $_conn;

var $_bind;
// default constructor

function adLDAP(){

//connect to the LDAP server as the username/password

$this->_conn = ldap_connect($this->random_controller());

ldap_set_option($this->_conn, LDAP_OPT_PROTOCOL_VERSION, 3);

ldap_set_option($this->_conn, LDAP_OPT_REFERRALS, 0); //disable plain text passwords

return true;

}
// default destructor

function __destruct(){ ldap_close ($this->_conn); }
function random_controller(){

//select a random domain controller

mt_srand(doubleval(microtime()) * 100000000);

return ($this->_domain_controllers[array_rand($this->_domain_controllers)]);

}
// authenticate($username,$password)

// Authenticate to the directory with a specific username and password

// Extremely useful for validating login credentials

function authenticate($username,$password){

//validate a users login credentials

$returnval=false;



if ($username!=NULL && $password!=NULL){ //prevent null bind

$this->_user_dn=$username.$this->_account_suffix;

$this->_user_pass=$password;



$this->_bind = @ldap_bind($this->_conn,$this->_user_dn,$this->_user_pass);

if ($this->_bind){ $returnval=true; }

}

return ($returnval);

}



// rebind()

// Binds to the directory with the default search username and password

// specified above.

function rebind(){

//connect with another account to search with if necessary

$ad_dn=$this->_ad_username.$this->_account_suffix;

$this->_bind = @ldap_bind($this->_conn,$ad_dn,$this->_ad_password);

if ($this->_bind){ return (true); }

return (false);

}
// user_info($user,$fields)

// Returns an array of information for a specific user

function user_info($user,$fields=NULL){

if ($user!=NULL){

if ($this->_ad_username!=NULL){ $this->rebind(); } //bind as a another account if necessary



if ($this->_bind){ //perform the search and grab all their details

$filter="samaccountname=".$user;

if ($fields==NULL){

$fields=array("samaccountname","mail","memberof","department","displayname","telephonenumber","primarygroupid");

//$fields=array("*");

}

$sr=ldap_search($this->_conn,$this->_base_dn,$filter,$fields);

$entries = ldap_get_entries($this->_conn, $sr);



// AD does not return the primary group in the ldap query, we may need to fudge it

if ($this->_real_primarygroup){

$entries[0]["memberof"][]=$this->group_cn($entries[0]["primarygroupid"][0]);

} else {

$entries[0]["memberof"][]="CN=Domain Users,CN=Users,".$this->_base_dn;

}



//echo ("<pre>"); print_r($entries);
$entries[0]["memberof"]["count"]++;

return ($entries);

}

}
return (false);

}



// user_groups($user)

// Returns an array of groups that a user is a member off

function user_groups($user,$recursive=NULL){

if ($this->_ad_username!=NULL){ $this->rebind(); } //bind as a another account if necessary

if ($recursive==NULL){ $recursive=$this->_recursive_groups; }



if ($this->_bind){

//search the directory for their information

$info=@$this->user_info($user,array("memberof"));

//echo ("<pre>"); print_r($info);

$groups=$info[0]["memberof"]; //presuming the entry returned is our guy (unique usernames)



$group_array=$this->nice_names($groups);
if ($recursive){

foreach ($group_array as $id => $group_name){

$extra_groups=$this->recursive_groups($group_name);

$group_array=array_merge($group_array,$extra_groups);

}

}



return ($group_array);

}

return (false);

}



// user_ingroup($user,$group)

// Returns true if the user is a member of the group

function user_ingroup($user,$group,$recursive=NULL){

if ($recursive==NULL){ $recursive=$this->_recursive_groups; }



if (($user!=NULL) && ($group!=NULL)){

if ($this->_ad_username!=NULL){ $this->rebind(); } //bind as a another account if necessary
if ($this->_bind){

$groups=$this->user_groups($user,array("memberof"),$recursive);

if (in_array($group,$groups)){ return (true); }

}

}

return (false);

}



function recursive_groups($group){

$ret_groups=array();



$groups=$this->group_info($group,array("memberof"));

$groups=$groups[0]["memberof"];
if ($groups){

$group_names=$this->nice_names($groups);

$ret_groups=array_merge($ret_groups,$group_names); //final groups to return



foreach ($group_names as $id => $group_name){

$child_groups=$this->recursive_groups($group_name);

$ret_groups=array_merge($ret_groups,$child_groups);

}
}
return ($ret_groups);

}
// take an ldap query and return the nice names, without all the LDAP prefixes (eg. CN, DN)

function nice_names($groups){
$group_array=array();

for ($i=0; $i<$groups["count"]; $i++){ //for each group

$line=$groups[$i];



if (strlen($line)>0){

//more presumptions, they're all prefixed with CN=

//so we ditch the first three characters and the group

//name goes up to the first comma

$bits=explode(",",$line);

$group_array[]=substr($bits[0],3,(strlen($bits[0])-3));

}

}

return ($group_array);

}


function group_cn($gid){

if ($this->_ad_username!=NULL){ $this->rebind(); } //bind as a another account if necessary



// coping with AD not returning the primary group

// http://support.microsoft.com/?kbid=321360

// for some reason it's not possible to search on primarygrouptoken=XXX

// if someone can show otherwise, I'd like to know about it :)

// this way is resource intensive and generally a pain in the @#%^



$r=false;



if ($this->_bind){

$filter="(&(objectCategory=group)(samaccounttype=". ADLDAP_SECURITY_GLOBAL_GROUP ."))";

$fields=array("primarygrouptoken","samaccountname","distinguishedname");

$sr=ldap_search($this->_conn,$this->_base_dn,$filter,$fields);

$entries = ldap_get_entries($this->_conn, $sr);



for ($i=0; $i<$entries["count"]; $i++){

if ($entries[$i]["primarygrouptoken"][0]==$gid){

$r=$entries[$i]["distinguishedname"][0];

$i=$entries["count"];

}

}

}

return ($r);

}



// group_info($group_name,$fields=NULL)

// Returns an array of information for a specified group

function group_info($group_name,$fields=NULL){

if ($this->_ad_username!=NULL){ $this->rebind(); } //bind as a another account if necessary
if ($this->_bind){

//escape brackets

$group_name=str_replace("(","\(",$group_name);

$group_name=str_replace(")","\)",$group_name);



$filter="(&(objectCategory=group)(name=".$group_name."))";

//echo ($filter."<br>");

if ($fields==NULL){ $fields=array("member","memberof","cn","description","distinguishedname","objectcategory","samaccountname"); }

$sr=ldap_search($this->_conn,$this->_base_dn,$filter,$fields);

$entries = ldap_get_entries($this->_conn, $sr);

//print_r($entries);

return ($entries);

}

return (false);

}
function all_users($include_desc = false, $search = "*", $sorted = true){

// Returns all AD users

if ($this->_ad_username!=NULL){ $this->rebind(); } //bind as a another account if necessary



if ($this->_bind){

$users_array = array();



//perform the search and grab all their details

$filter = "(&(objectClass=user)(samaccounttype=". ADLDAP_NORMAL_ACCOUNT .")(objectCategory=person)(cn=$search))";

$fields=array("samaccountname","displayname");

$sr=ldap_search($this->_conn,$this->_base_dn,$filter,$fields);

$entries = ldap_get_entries($this->_conn, $sr);



for ($i=0; $i<$entries["count"]; $i++){

if( $include_desc && strlen($entries[$i]["displayname"][0]) > 0 )

$users_array[ $entries[$i]["samaccountname"][0] ] = $entries[$i]["displayname"][0];

else if( $include_desc )

$users_array[ $entries[$i]["samaccountname"][0] ] = $entries[$i]["samaccountname"][0];

else

array_push($users_array, $entries[$i]["samaccountname"][0]);

}

if( $sorted ){ asort($users_array); }

return ($users_array);

}

return (false);

}



function all_groups($include_desc = false, $search = "*", $sorted = true){

// Returns all AD groups

if ($this->_ad_username!=NULL){ $this->rebind(); } //bind as a another account if necessary



if ($this->_bind){

$groups_array = array();



//perform the search and grab all their details

$filter = "(&(objectCategory=group)(samaccounttype=". ADLDAP_SECURITY_GLOBAL_GROUP .")(cn=$search))";

$fields=array("samaccountname","description");

$sr=ldap_search($this->_conn,$this->_base_dn,$filter,$fields);

$entries = ldap_get_entries($this->_conn, $sr);



for ($i=0; $i<$entries["count"]; $i++){

if( $include_desc && strlen($entries[$i]["description"][0]) > 0 )

$groups_array[ $entries[$i]["samaccountname"][0] ] = $entries[$i]["description"][0];

else if( $include_desc )

$groups_array[ $entries[$i]["samaccountname"][0] ] = $entries[$i]["samaccountname"][0];

else
array_push($groups_array, $entries[$i]["samaccountname"][0]);

}

if( $sorted ){ asort($groups_array); }

return ($groups_array);

}

return (false);

}

} // End class
?>


That needs to go into a directory called LDAP inside the main output directory.
Now change login.php to suit, make sure you build the project with users logging in with Username and password from database, have a users table in the database, make sure that the group levels for different access are in there too.

I use a `staff` table, with the login field set to the same as the staff login for the local network, including capitals where they occur.

Before I set up LDAP, i use their lastname as the password just for testing, then once I am happy with the application, I build in LDAP.

OK, change the login.php file to suit this one here, there are other changes apart from those in the section marked LDAP, so do a side by side compare.

<?php

//login php from graeme forrester works with V3.1
//modified by GNN and works with v4.2 build 358

ini_set("display_errors","1");

ini_set("display_startup_errors","1");

set_magic_quotes_runtime(0);
include("include/dbcommon.php");
if(@$_POST["a"]=="logout" || @$_GET["a"]=="logout")

{

session_unset();

setcookie("username","",time()-365*1440*60);

setcookie("password","",time()-365*1440*60);

header("Location: login.php");

exit();

}

include('libs/Smarty.class.php');

$smarty = new Smarty();
$conn=db_connect();
// Before Process event

if(function_exists("BeforeProcessLogin"))

BeforeProcessLogin($conn);





$myurl=@$_SESSION["MyURL"];

unset($_SESSION["MyURL"]);
$defaulturl="";

$defaulturl="nd_pupils_list.php";
$message="";
$pUsername=postvalue("username");

$pPassword=postvalue("password");
if(@$_COOKIE["username"] || @$_COOKIE["password"])

$smarty->assign("checked"," checked");
if (@$_POST["btnSubmit"] == "Login")

{

if(@$_POST["remember_password"] == 1)

{

setcookie("username",$pUsername,time()+365*1440*60);

setcookie("password",$pPassword,time()+365*1440*60);

$smarty->assign("checked"," checked");

}

else

{

setcookie("username","",time()-365*1440*60);

setcookie("password","",time()-365*1440*60);

$smarty->assign("checked","");

}

// username and password are stored in the database

$strUsername = (string)$pUsername;

$strPassword = (string)$pPassword;

$sUsername=$strUsername;

$sPassword=$strPassword;

$strlUsername = postvalue("username");

$strlPassword = postvalue("password");



$rstemp=db_query("select * from `staff` where 1=0",$conn);



if(FieldNeedQuotes($rstemp,$cUserNameField))

$strUsername="'".db_addslashes($strUsername)."'";

else

$strUsername=(0+$strUsername);

if(FieldNeedQuotes($rstemp,$cPasswordField))

$strPassword="'".db_addslashes($strPassword)."'";

else

$strPassword=(0+$strPassword);
$strSQL = "select * from `staff` where ".AddFieldWrappers($cUserNameField)."=".$strUsername." and ".AddFieldWrappers($cPasswordField)."=".$strPassword;



################################################################################

##

# LDAP AUTHENTICATION MODIFICATION #

################################################################################

##
//include the class

include ("ldap/adLDAP.php");
//create the LDAP connection

$adldap = new adLDAP();

$ldap_auth = 0;

$ldap_group ="YourGroupName"; # Specified group for group authentication, if you use group authentication
// Authenticate

if (($adldap -> authenticate($strlUsername,$strlPassword))){

if ($adldap -> user_ingroup($strlUsername,$ldap_group)){ # Group Authentication Only

$ldap_auth = 1;
// Check if user exists

$sql = "Select * from `staff` Where ".AddFieldWrappers($cUserNameField)." = \"$strlUsername\"";
$rs = mysql_query($sql,$conn) or die("USER QUERY FAILED.");
// Generate Query

$strSQL = "select * from `YourUserTable` where ".AddFieldWrappers($cUserNameField)."=\"".$strlUsername."\"";
} # EndGroup Authentication Only

}
// Catch failed logins

if ($ldap_auth == 0)$strSQL = "select * from `YourUserTable` where ".AddFieldWrappers($cUserNameField)."=\"xxxxx\"";
################################################################################

##

# END OF MODIFICATION #

################################################################################

##
if(function_exists("BeforeLogin")){

if(!BeforeLogin($pUsername,$pPassword))

$strSQL="select * from `YourUserTable` where 1<0";

}

$rs=db_query($strSQL,$conn);

$data=db_fetch_array($rs);
// if($data && @$data[$cUserNameField]==$sUsername && @$data[$cPasswordField]==$sPassword)

if($data && @$data[$cUserNameField]==$sUsername)
//if($data && $ldap_auth)

{
$_SESSION["UserID"] = $pUsername;

$_SESSION["AccessLevel"] = ACCESS_LEVEL_USER;

$_SESSION["GroupID"] = $data["level"];

$_SESSION["OwnerID"] = $data["userid"];

$_SESSION["_comments_OwnerID"] = $data["userid"];

if(function_exists("AfterSuccessfulLogin"))

AfterSuccessfulLogin($pUsername,$pPassword,$data);

if($myurl)

header("Location: ".$myurl);

else

header("Location: ".$defaulturl);

return;

}

else

{

if(function_exists("AfterUnsuccessfulLogin"))

AfterUnsuccessfulLogin($pUsername,$pPassword);

$message = "Invalid Login";

}

}
$_SESSION["MyURL"]=$myurl;

if($myurl)

$smarty->assign("url",$myurl);

else

$smarty->assign("url",$defaulturl);
if(@$_POST["username"] || @$_GET["username"])

$smarty->assign("value_username","value=\"".htmlspecialchars($pUsername)."\"");

else

$smarty->assign("value_username","value=\"".htmlspecialchars(refine(@$_COOKIE["username"]))."\"");
if(@$_POST["password"])

$smarty->assign("value_password","value=\"".htmlspecialchars($pPassword)."\"");

else

$smarty->assign("value_password","value=\"".htmlspecialchars(refine(@$_COOKIE["password"]))."\"");
if(@$_GET["message"]=="expired")

$message = "Your session has expired. Please login again.";
$smarty->assign("message",$message);
$templatefile="login.htm";

if(function_exists("BeforeShowLogin"))

BeforeShowLogin($smarty,$templatefile);
$smarty->display($templatefile);

?>


Once you have got it working, make a safety copy of the two files you have had to alter and keep them well away from the web pages.

I can almost guarantee that you will overwrite the login.php file on one rebuild, then you have it all to do again.
Good luck and do lots of reading before you start