http://www.developer.com/

Back to article

Integrating Active Directory with PHP


October 29, 2003

Software engineering is not unlike other industrial craft; the actors in each respective trade are engaged in the practice of creating something out of nothing. Often their products are the result of skills within several disciplines. For example, a furniture designer might require knowledge working within wood, metal, and glass, while often a software product's quality is often the function of the software designer's knowledge and experience working with programming languages, networking, databases, and user interface design.

An Internet application developer at the Fisher College of Business, I can certainly relate to this idea. We've spent the last several months developing and deploying Active Directory and Exchange to our 1,000+ faculty, staff and graduate students. Like many organizations, we run a heterogeneous environment consisting of Windows, Solaris, and Linux servers, and dominated by Windows workstations. While we've decided that a Microsoft-based e-mail/calendaring system (Active Directory and Exchange) would best suit our user's needs (and I dare say that the majority of our users would agree with this choice), we're particularly fond of continuing to use Open Source solutions for our Web and database services, namely Linux, the Apache Web server, and MySQL. Despite this mixed environment, we're still adamant about providing services like single sign-on, online contact directories, and content management tools. In our situation, such services can only be implemented through cross-platform communication. In many cases, this requires the integration of PHP, Perl, Apache, and Microsoft's Active Directory. This topic is the focus of this article series.

Within this and the coming articles, I'll highlight key concepts pertinent to implementing services requiring Active Directory/PHP/Perl/Apache integration. I'll assume that you're already familiar with basic Active Directory data schemas, in addition to general LDAP syntax. Furthermore, I'll take for granted that you're familiar with PHP and Perl language syntax, and with Apache's configuration syntax. Finally, because administration-level changes could be necessary, you'll need privileged control, or at least the administrator's willingness to made changes for you.

On an aside, although all directory server products differ in general implementation, those which are based on the LDAP standard generally allow for examples based on one product to be easily converted to another. Therefore, if you happen to be working with a product other than Active Directory, OpenLDAP for instance, all examples provided in this and the following articles, unless otherwise noted, will work as-is.

In this first installment, we'll take a look at PHP's LDAP support, basing our examples on Microsoft's Active Directory implementation. After an introduction of the prerequisite configuration tasks and core PHP functions, we'll provide a highly applicable example demonstrating the creation of a Web-based Active Directory search interface.

Prerequisites

Prior to executing any of the examples offered in this article, you'll need to make sure that a few prerequisite tasks are accomplished. Each of these tasks are outlined in this section.

PHP Configuration

PHP's LDAP support is not enabled by default. If you haven't already done so, you'll have to rebuild PHP, this time including the LDAP extension. This is done by passing the—with-ldap flag during the configuration phase of the build process.

%>./configure --with-ldap [OTHER OPTIONS]

If you're building PHP as a shared module, remember that you won't need to rebuild Apache after making these changes! If you don't know what I'm talking about, and are wondering how such a convenience could be afforded to you, see the PHP manual for complete installation and configuration instructions.

A Privileged Account

One's ability to manage institutional resources is directly dependent upon furnishable credentials. The directory service security model is no different; credentials must be furnished before resources can be accessed. This process, known as binding, involves tying a particular set of credentials (username and password) to the connection. These credentials are established within your respective directory server product. In Active Directory, this simply involves assigning the necessary privileges to a user. If you haven't done so already, I'd like to recommend creating a new user which will be used specifically for PHP-initiated connections. For the purposes of this article, I'll call that user "ad-web." Because the user domain must also be declared, this user will be referenced as "ad-web@ad.wjgilmore.com." The password will be the highly cryptic word "secret."

Firewall Adjustments

If you intend to work with a directory server residing outside of your local network, some firewall adjustments may be required. By default, LDAP connections take place over port 389; if your directory server supports secure connections (LDAPS), you'll need to open port 636. Therefore, you'll need to adjust your firewall to allow for access via at least these two ports, if not already enabled.

Key PHP Functions

The first four functions introduced in this section are practically omnipresent whenever communicating with LDAP via PHP. These functions include ldap_connect(), ldap_set_option(), ldap_bind(), and ldap_unbind(), and they're responsible for connecting, setting configuration parameters, binding to, and closing the directory server connection. Each is introduced in this section.

ldap_connect()

resource ldap_connect([string hostname [, integer port]])
Prior to doing anything with an LDAP server, a connection must be established. You can do so with ldap_connect(), passing in an optional hostname and port if the connection isn't local. An example follows:

<?php
    ldap_connect("ad.wjgilmore.com") or
                  die("Couldn't connect to AD!");
?>

Once connected to the server, many implementations require that you specify which LDAP protocol version you'll be using. This is accomplished by using ldap_set_option(), introduced next.

ldap_set_option()

boolean ldap_set_option(resource link_id, int option, mixed newval)
Unlike Perl, PHP's LDAP connection function does not offer a means for declaring the intended protocol version within the connection function. Therefore, after successfully establishing a connection, you'll likely need to declare this version by setting PHP's constant LDAP_OPT_PROTOCOL_VERSION. This constant, one of twelve capable of being modified via this function, determines which protocol version should be used. An example follows:

<?php
    $ad = ldap_connect("ldap://ad.wjgilmore.com") or
          die("Couldn't connect to AD!");
    ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);
?>

Keep in mind that in my experience this function is a requirement for executing certain directory server tasks via an API. However, this may not be the case with all APIs.

ldap_bind()

boolean ldap_bind(resource link_id [, string bind_rdn [, string bind_pswd]])
Entering a restricted area doesn't imply that you have free reign over its secrets. Exactly what material you can access, and whether you can add, modify, or destroy the contents is dependent upon your furnishable credentials. Directory Services' security model is based on the same concept. Although anybody can establish a connection with a visible directory server, credentials must be supplied before anything can be done with that connection. The PHP function used to supply this credentials is ldap_bind().

<?php
    $ad = ldap_connect("ldap://ad.wjgilmore.com")
          or die("Couldn't connect to AD!");
    $bd = ldap_bind($ad,"ad-web@ad.wjgilmore.com","secret")
          or die("Couldn't bind to AD!");
?>

Once a set of credentials have been successfully bound to the server, you can begin carrying out queries against it.

ldap_unbind()

boolean ldap_unbind(resource link_id)
Once all directory server-specific tasks have been completed, the connection should be closed. This is accomplished by using the ldap_unbind() function. An example follows:

<?php
    // Connect to the directory server.
    $ad = ldap_connect("ldap://ad.wjgilmore.com") 
          or die("Couldn't connect to AD!");

    // Bind to the directory server.
    $bd = ldap_bind($ad,"ad-web@ad.wjgilmore.com","secret") or
          die("Couldn't bind to AD!");

    // Carry out directory server-specific tasks.

    // Close the connection
    ldap_unbind($ad);
?>

Although necessary, the aforementioned functions aren't very exciting. Not to worry, as we're not finished yet; the remainder of this article is devoted to actually retrieving and managing the data located in your directory server.

Searching and Manipulating Active Directory Data

In this section, you'll learn how to search and retrieve data from the directory server, as well as add, modify, and delete entries.

ldap_search()

resource ldap_search ( resource link_identifier, string base_dn, string filter [, array attributes [, int attrsonly [, int sizelimit [, int timelimit [, int deref]]]]])
The ldap_search() function offers a powerful means for searching the directory server pointed to by link_identifier. It will search to a depth of LDAP_SCOPE_SUBTREE, a value that can be set via the previously introduced function ldap_set_option(). By default, this value is set to search to an infinite depth, or through the entire scope of the tree as defined by the base_dn. The search filter, equivalent to a relational database query, is passed in via the filter parameter. Finally, you can specify exactly which attributes should be returned within the search results via the attributes parameter. The remaining four parameters are optional, and therefore in the interests of space, I'll leave it as an exercise to you to learn more about them. Let's consider an example:

<?php

    $dn = "OU=People,OU=staff,DN=ad,DN=wjgilmore,DN=com";

    $attributes = array("displayname", "l");

    $filter = "(cn=*)";

    $ad = ldap_connect("ldap://ad.wjgilmore.com")
          or die("Couldn't connect to AD!");
  
    ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);

    $bd = ldap_bind($ad,"ad-web@ad.wjgilmore.com","secret")
          or die("Couldn't bind to AD!");

    $result = ldap_search($ad, $dn, $filter, $attributes);

    $entries = ldap_get_entries($ad, $result);

    for ($i=0; $i<$entries["count"]; $i++)
    {
        echo $entries[$i]["displayname"]
             [0]."(".$entries[$i]["l"][0].")<br />";
    }

    ldap_unbind($ad);

?>

A sampling of the results follow:

Gilmore, Jason (Columbus)
Shoberg, Jon (Columbus)
Streicher, Martin (San Francisco)
Wade, Matt (Orlando)

Most of this is likely straightforward, save for the potentially odd way in which attribute values are referenced. All attribute rows are ultimately multi-dimensional arrays, with each attribute value referenced by a combination of row number, attribute name, and attribute array index. So, for example, even attributes such as "sn", the attribute name for the user's last name, is an indexed array.

ldap_mod_add()

boolean ldap_mod_add(resource link_id, string dn, array entry)
Adding entries to the directory server is accomplished via the ldap_mod_add() function. A new entry is added simply by creating an array consisting of the attribute/value mappings intended to comprise the new row. This process is perhaps best explained with an example:

<?php
    $dn = "OU=People,OU=staff,DN=ad,DN=wjgilmore,DN=com";

    $ad = ldap_connect("ldap://ad.wjgilmore.com")
          or die("Couldn't connect to AD!");

    ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);

    $bd = ldap_bind($ad,"ad-web@ad.wjgilmore.com","secret") 
          or die("Couldn't bind to AD!");

    $user["givenname"] = "Jason";
    $user["sn"] = "Gilmore";
    $user["mail"][] = "jason@wjgilmore.com";

    $result = ldap_mod_add($ad, $dn, $user);

    if ($result) echo "User added!" else
                 echo "There was a problem!";

    ldap_unbind($ad);

?>

As is the case with all directory server tasks, be sure that the binding user has proper permissions to add the target data; otherwise, errors will occur.

ldap_mod_replace()

boolean ldap_mod_replace(resource link_id, string dn, array entry)
Modifying entry attributes is accomplished via the ldap_mod_replace() function. It operates exactly like ldap_add(), save for the added step of identifying the entry you'd like to modify. This is done by pointing to a very specific dn. Like ldap_add(), both a valid link identifier and an array consisting of the entries you'd like to update must be provided. An example follows, demonstrating how a user's telephone number would be modified. In particular, take note of the very specific DN (pointing to my particular entry).

<?php
    $dn = "CN=Jason Gilmore,OU=People,OU=staff,DN=ad,
           DN=wjgilmore,DN=com";

    $ad = ldap_connect("ldap://ad.wjgilmore.com")
          or die("Couldn't connect to AD!");

    ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);

    $bd = ldap_bind($ad,"ad-web@ad.wjgilmore.com","secret")
          or die("Couldn't bind to AD!");

    $user["telephonenumber"] = "614-999-9999";

    $result = ldap_mod_replace($ad, $dn, $user);
    if ($result) echo "User modified!" else
                 echo "There was a problem!";

    ldap_unbind($ad);

?>

As is the case with all directory server tasks, be sure that the binding user has proper permissions to modify the target data; otherwise, unexpected errors will occur.

ldap_delete()

boolean ldap_delete(resource link_id, string dn)
Rounding out our survey of key PHP LDAP functions is ldap_delete(). This function is used to delete an existing entry. Like ldap_mod_replace(), a very specific DN must be provided to effect the deletion. The following example demonstrates how to remove the "Jason Gilmore" user entry from Active Directory:

<?php
    $dn = "CN=Jason Gilmore,OU=People,OU=staff,DN=ad,
           DN=wjgilmore,DN=com";

    $ad = ldap_connect("ldap://ad.wjgilmore.com")
          or die("Couldn't connect to AD!");
  
    ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3);

    $bd = ldap_bind($ad,"ad-web@ad.wjgilmore.com","secret") 
          or die("Couldn't bind to AD!");

    $result = ldap_delete($ad, $dn);
    if ($result) echo "User deleted!" else
                 echo "There was a problem!";

    ldap_unbind($ad);

?>

As is the case with all directory server tasks, be sure that the binding user has proper permissions to delete the target data; otherwise, unexpected errors will occur.

Searching Active Directory via the Web

I always like to round out a tutorial with an applicable example that readers can immediately adapt to their own needs. In this tutorial, I'll show you how to create a search interface capable of searching by name, location, or phone number. All you'll need to do is modify the connection variables and base DN. To begin, let's create the search interface, which will be saved as "search.html":

<p>
  <form action="search.html" method="post">
    Search criteria:<br />
    <input type="text" name="keyword" size="20"
           maxlength="20" value="" /><br />
    Filter:<br />
    <select name="filter">
        <option value="">Choose One:</option>
        <option value="sn">Last Name</option>
        <option value="telephonenumber">Phone</option>
        <option value="l">City</option>
    </select><br />
    <input type="submit" value="Search!" />
  </form>
</p>

Figure 1 offers an example of what this search form would look like in the browser.

Figure 1. The Active Directory Search Form

Next, we'll need to create the logic that effects the search. This short bit of code is shown here:

<?php

// Designate a few variables
$host = "ldap://ad.gilmore.com";
$user = "ad-web@ad.wjgilmore.com";
$pswd = "secret";

$ad = ldap_connect($host)
      or die( "Could not connect!" );

// Set version number
ldap_set_option($ad, LDAP_OPT_PROTOCOL_VERSION, 3)
     or die ("Could not set ldap protocol");

// Binding to ldap server
$bd = ldap_bind($ad, $user, $pswd)
      or die ("Could not bind");

// Create the DN
$dn = "OU=People,OU=staff,DN=ad,DN=wjgilmore,DN=com";

// Specify only those parameters we're interested in displaying
$attrs = array("displayname","mail","telephonenumber");

// Create the filter from the search parameters
$filter = $_POST['filter']."=".$_POST['keyword']."*";

$search = ldap_search($ad, $dn, $filter, $attrs)
          or die ("ldap search failed");

$entries = ldap_get_entries($ad, $search);

if ($entries["count"] > 0) {

for ($i=0; $i<$entries["count"]; $i++) {
   echo "<p>Name: ".$entries[$i]["displayname"][0]."<br />";
   echo "Phone: ".$entries[$i]["telephonenumber"][0]."<br />";
   echo "Email: ".$entries[$i]["mail"][0]."</p>";
}

} else {
   echo "<p>No results found!</p>";
}

ldap_unbind($ad);

?>

You can either change the action destination specified in the search interface, pointing it to a file consisting of the above script, or you can bundle it into the same file as the search interface, and use isset() and an if conditional to trigger execution in the case that the search submit button is depressed. Of course, you'll want to add some additional data validation criteria prior to deploying such a script. Figure 2 offers a sampling of the search results.

Figure 2. Search Results

Conclusion

Although PHP has long been my primary language for developing Web applications, I've found Perl to be an integral part of my programmer's toolkit. When working with directory servers, this sentiment is no different. Therefore, the next article is devoted to Perl/LDAP basics. As was the case with this article, all examples are specific to Microsoft's Active Directory, although you should be able to easily apply them to any directory server implementation. We'll round out that article with an example demonstrating how to create statically cached Web-based user directories using a Perl script and CRON (or Windows Task Scheduler).

I welcome questions and comments! E-mail me at jason@wjgilmore.com. I'd also like to hear more about your experiences integrating Microsoft and Open Source technologies!

About the Author

W. Jason Gilmore (http://www.wjgilmore.com/ ) is an Internet application developer for the Fisher College of Business. He's the author of the upcoming book, PHP 5 and MySQL: Novice to Pro, due out by Apress in 2004. His work has been featured within many of the computing industry's leading publications, including Linux Magazine, O'Reillynet, Devshed, Zend.com, and Webreview. Jason is also the author of A Programmer's Introduction to PHP 4.0 (453pp., Apress). Along with colleague Jon Shoberg, he's co-author of "Out in the Open," a monthly column published within Linux magazine.

Sitemap | Contact Us

Thanks for your registration, follow us on our social networks to keep up-to-date