Integrating LDAP with Perl and Apache

Clayton Donley

Motorola, Inc.

< qa1049@email.mot.com >

Perl Conference 2.0

1.0 Abstract

The Lightweight Directory Access Protocol (LDAP) is the Internet standard for accessing on-line directories. Directories supporting LDAP can be leveraged within applications and scripts to provide a number of benefits.

This document introduces LDAP and provides a foundation for accessing LDAP-enabled directory services from Perl. Emphasis in placed on using Perl and LDAP within Apache and Common Gateway Interfaces (CGIs) to provide advanced authentication, access control, configuration, personalization, and redirection. Performance and security aspects of LDAP services are also discussed where they would affect on-line applications or the Apache web server.

2.0 Introduction to LDAP

Directory services are a key component of any network infrastructure. Just as a phone book provides an interface for finding access numbers on a telephone network, on-line directory services provide the means for finding information about people, places, and things on a computer network.

With the explosive growth of the Internet, it becomes increasingly difficult to identify communications partners and resources in a standard way. In the past few years, the Lightweight Directory Access Protocol (LDAP) has emerged as the dominant Internet standard for accessing this type of information in on-line directories.

As the name implies, LDAP was originally designed as a lightweight replacement for DAP, an OSI protocol for accessing directory services based on the International Telecommunications Union (ITU) X.500 standard. Many LDAP implementations have since divorced themselves from their X.500 roots and implemented stand-alone services. While these services do not rely on X.500, they do share a similar information model.

Information in an LDAP-based directory service is stored in entries. Each entry belongs to one or more object classes that specify the type of entry being stored within the directory. Examples of object classes might include "country", "organization", and "person". Each piece of data in an entry is stored within an attribute. Object class membership determines which types of attributes an entry can store. For example, an entry for a "person" object would contain names, addresses, phone numbers, and other information pertaining to people. Depending on type, an attribute will contain one or more values.

Each entry in an LDAP-based directory service has a unique name associated with it. This "distinguished name" (DN) is made up of a comma separated string of "relative distinguished names" (RDN) that together specify an entry's location and name within the directory tree. A relative distinguished name is made up of one or more attribute/value pairs that are unique at their level in the directory tree.

LDAP servers vary in the way that these entries are physically stored. Most stand-alone LDAP servers use simple b-tree and hash based databases. Others offer the option of using commercial database back-ends. More recently, LDAP has emerged as the access protocol for meta-directories. Meta-directories may not physically store data themselves, but rather would use LDAP as an interface to provide information stored in one or more proprietary directories. This is similar to the way that LDAP was initially used with X.500 directories.

While implementations vary in how data is physically represented, these servers all speak the same language, LDAP. Like many other Internet protocols (i.e. Hyper-Text Transfer Protocol and Simple Mail Transport Protocol), LDAP is a TCP protocol. Unlike these other Internet protocols, which use simple text messages for sending request and receiving results, LDAP transmissions are specially encoded.

LDAP tranmissions are defined in Abstract Syntax Notation One, or ASN.1. ASN.1 is a language defined by ITU-T X.208 for developing transmission structures. Prior to transmission, the ASN.1 structure containing an LDAP request or result is encoded using the Basic Encoding Rules (BER) defined in ITU-T X.209. The final BER encoded structure is then transmitted over a stream.

While BER and ASN.1 are beyond the scope of this paper, a good introduction can be found on the Internet at ftp://ftp.rsa.com/pub/pkcs/ascii/layman.asc . It is important to understand that the requirement for this special encoding would normally make it both difficult and time consuming to write programs that can speak LDAP.

Fortunately, one of the reasons for LDAP's growing popularity is that a standard, documented application program interface (API) exists that allows programmers to access LDAP directories without handling any of the encoding/decoding necessary to actually talk to the server. These APIs are available on numerous platforms, including Windows, MacOS, and most flavors of Unix.

3.0 Perl and LDAP

While the C APIs are wonderful for enabling LDAP support in traditional applications, a Perl interface is very useful for enabling LDAP support in online applications and system administration tools. As of June, 1998, there are two actively supported Perl modules supporting LDAP.

The first, Net::LDAP from Graham Barr ( http://www.connect.net/gbarr/ ), takes the ambitious step of trying to implement an LDAP API, complete with BER encoding/decoding, in Perl. Although the module is unfinished and in Alpha, it does provide some of the most common commands for accessing and maintaining an LDAP directory.

The other, Net::LDAPapi, implements a XS wrapper to provide Perl methods for all the functions in the standard LDAP C APIs. In addition to being an easier module to develop, the fact that it still calls C functions internally for encoding and decoding LDAP requests would likely make this module run faster than the Net::LDAP implementation.

Method interfaces for the two modules are not currently compatible, though this may be corrected in the future. All information and examples in this document are pertinent only to the Net::LDAPapi module.

3.1 Installing Net::LDAPapi

Installation of the Net::LDAPapi module requires that Perl 5.004 and a LDAP developer kit already be installed on the system. Building the module from source, necessary on all platforms except Windows NT, also requires a C compiler.

While the module can be built from source to work with any standard LDAPv2 or LDAPv3 developers' kit, most testing has been performed using kits from Netscape and the University of Michigan. The binary for Windows NT currently requires the Netscape developer kit for NT. These kits can be found as follows:

Libraries included or built in these kits should be copied to standard directories (i.e. /usr/local/lib, \winnt\system32). For future reference, the location of the files ldap.h and lber.h should be noted.

With installation of the LDAP C developers' kit out of the way, it should now be possible to retrieve the module itself from either a Comprehensive Perl Archive Network (CPAN) mirror or http://www.wwa.com/~donley/ .

If installing the Windows NT binary, simply unzip the appropriate archive into Perl's parent directory. For example, if Perl lives in C:\Perl, extract the archive to C:\. Once this is done, the module should be immediately usable. Check the included README file for any further instructions.

To build the module from source, simply extract the archive into a temporary directory and change into the module's root directory. Next, run the Perl interpreter on the file "Makefile.PL". This will ask a few questions related to the LDAP C developer kit in use and generate an appropriate "Makefile" for the current environment.

 
$ perl Makefile.PL

The module can now be built by running `make', or its equivalent on the platform being used (i.e. Visual C++'s `nmake' or the GNU `gmake'). Prior to final installation, it is important to test the module using `make test' to ensure proper operation. The test will query the name of a LDAP server, search filter, and base distinguished name prior to performing LDAP search related tests.

 
$ make
$ make test

Once tests have passed, the module can be automatically installed by issuing the `make install' command. The module is now ready for action.

 
# make install

3.2 Searching the Directory

The easiest way to start using LDAP in Perl scripts is to analyze an example. The following script demonstrates a simple LDAP search.

 
# First include the LDAP module
use Net::LDAPapi;

# $BASE is the top level in the tree
$BASE = "o=Org, c=US";

# $SCOPE is one of
#   LDAP_SCOPE_SUBTREE
#     - At or Below $BASE
#   LDAP_SCOPE_ONELEVEL
#     - At or One Level Below $BASE
#   LDAP_SCOPE_BASE
#     - At $BASE Only
$SCOPE = LDAP_SCOPE_SUBTREE;

# $FILTER is the LDAP search filter
$FILTER = "mail=abc123@org.com";

# $ATTRIBS is a reference to a list of
#   attributes to return.
$ATTRIBS = ["cn","mail"]

# Create a new object in the
# Net::LDAPapi class
$ldap = new Net::LDAPapi("myhost.domain.com");

# Bind to the LDAP server anonymously
if ($ldap->bind_s != LDAP_SUCCESS)
{
# If Errors Occur, print them to
# STDERR, unbind, and die
   $ldap->perror("bind_s");
   $ldap->unbind;
   die;
}

# Search the LDAP directory.  The 0 in
# the last argument specifies that
# attribute values should be returned.
if ($ldap->search_s($BASE, $SCOPE, $FILTER, $ATTRIBS, 0) != LDAP_SUCCESS)
{
   $ldap->perror("search_s");
   $ldap->unbind;
   die;
}

# The get_all_entries class method
# returns a reference to a hash of
# hashes that contains all the
# returned information.
$entries = $ldap->get_all_entries;

# Unbind when finished
$ldap->unbind;

At this point, the reference $entries points all the results returned by the LDAP server. An entry might look something like:

 
"attr=Value, o=Org, c=US" =>
      {"Attribute" => ["V1",...,"Vn"]}

The following Perl segment demonstrates how one might parse and print all the returned entries.

 
# $entry is set to the DN of each entry
# in the hash reference
foreach $entry (keys %{$entries})
{
   print "\nDistinguished Name: " .
     $entry . "\n";

# $attribute is set to each attribute
   foreach $attribute (keys %{$entries{$entry}})
   {

#  Cycle through each $value and print
foreach $value (@{$entries{$entry}{$attribute}})
      {
         print "$attribute: $value\n";
      }
   }
}

This simple LDAP search program could be easily extended to take arguments that would allow it to offer more flexibility. Such a program could be used to make user friendly command line tools for directory lookups.

3.3 Changing the LDAP Directory

While the ability to perform LDAP searches from Perl is relatively helpful in itself, Perl's solid support for the manipulation of hashes, arrays, and strings make it an extremely powerful tool for making directory updates.

This is because LDAP attributes are represented as strings, as are most values. Each attribute may have one or more values, with attributes and values sharing a relationship similar to hashes that contain string keys and array references for values. Thus, when making changes to an LDAP directory, one first generates a hash containing the appropriate changes. A reference to this hash can then be passed to relevant add or modify method, which will properly encode them for transmission to the server.

3.3.1 Adding Entries

The following Perl code segment demonstrates how to build a hash of attributes and values that can be used to add an entry to the directory.

 
%ADD_HASH = (

# Add a multi-valued "objectclass"
# Must use a list reference
   "objectclass" => ["top","person"],

# Add "cn" with a single value
# Can use a list reference, not
# required
   "cn" => ["Joe Humanoid"],

# "sn" is also single-valued, but
# passed as a scalar, not a list
# reference
   "sn" => "Humanoid",

# Add a binary attribute, "jpegPhoto"
# $jpegphoto might contain the exact
# binary contents of a JPEG file.
   "jpegPhoto" => {"b" => $jpegphoto},
);

Once the hash has been created, the process for adding the entry is relatively easy. The following code segment shows how to use the previously generated %ADD_HASH to create a new entry in the directory.

 
# Create our Net::LDAPapi object with
# an open a connection to the server
$ldap = new Net::LDAPapi("myserver.my.com");

# Bind to the server as an
# administrator's DN and the
# clear-text password "secret"
if ($ldap->bind_s("uid=Admin, o=My, c=US","secret") != LDAP_SUCCESS)
{
   $ldap->perror("bind_s");
   $ldap->unbind;
   die;
}

# Add the specified DN with attributes
# and values as specified by
# %ADD_HASH.  Note that a reference to
# %ADD_HASH is passed to the add_s
# method.
if ($ldap->add_s("cn=Joe, o=Org, c=US", \%ADD_HASH) != LDAP_SUCCESS)
{
   $ldap->perror("add_s);
   $ldap->unbind;
   die;
}

# Unbind from the LDAP server
$ldap->unbind;

Note that if an empty password is passed to the bind_s method in the segment above, it will ALWAYS return success. This is called a reference bind and connects with privileges identical to an anonymous bind. Thus, while the bind is successful, attempts to modify the directory would be unsuccessful, as the connection is not actually bound as the distinguished name specified in the bind_s method.

3.3.2 Modifying Entries

Modifying an LDAP directory entry is similar to the process demonstrated above for adding entries. A reference to a hash of modifications is first generated and then passed as the second argument to the modify_s method. While a modify hash is similar to an add hash in most ways, there are a few extensions to support varying modification types.

The following Perl code segment shows some of the ways that a hash can be generated to modify the directory.

 
%MODIFY_HASH = (
# Remove the "jpegphoto" attribute
   "jpegphoto" => "",

# Replace "givenname" with a single value
   "givenName" => "Joe",

# Replace "sn" with multiple values
# "r" means replace
   "sn" => {"r" =>
       "Biped","Humanoid"},

# Add a single value to "cn", but do
# not affect existing values.
# "a" means add
   "cn",{"a" => ["Joe Biped"]},

# Delete a specific value from
# "telephonenumber".  Other values not
# affected.
# "d" means delete
  "telephoneNumber"=>{"d","555-1212"},
);

The "a" and "r" modification types may also be extended by adding a "b" to indicate the addition of binary values. For example:

 
"jpegphoto" => {"rb" => $jpegphoto}

After creating the modify hash, the same procedure is used to modify an entry that is used for adding an entry, except that the modify or modify_s method is used.

 
$ldap->modify_s($DN,\%MODIFY_HASH);

Once again, the connection being used to modify an entry needs to be bound as an entry with permission to make such changes.

3.3.3 Deleting Entries

The delete_s method is used to delete an entry from the directory. This method takes only the distinguished name of the entry to be deleted. Since no changes are being made within the entry, no hash reference is necessary.

 
$ldap->delete_s("cn=Joe, o=Org, c=US");

3.4 Authentication and Access Control

As noted in section 3.2, it is usually necessary to bind to the LDAP server prior to making changes. The bind_s method (or bind method in asynchronous operation) is responsible for authenticating the script to the directory server.

In all non-anonymous bind requests, the first argument is the distinguished name of the entry to authenticate. The second argument is the credentials to be passed to the server. LDAPv2 servers only support plain-text passwords for authentication. Some implementations of LDAPv2, such as the one from the University of Michigan, have been extended to support Kerberos credentials. Net::LDAPapi supports both, but defaults to plain-text passwords. This default may be changed by a third argument to the bind function specifying the authentication type.

This bind facility is a key component of LDAP based authentication. Given a specific DN and credentials, it is possible for a Perl script to use this information to bind to a specific server. A successful bind to the LDAP server indicates that the credentials were accepted and the user can be considered authenticated.

As mentioned previously, an empty credentials argument specifies a reference bind. While it may return success, it does not actually give the LDAP connection any privileges. While this doesn't mean much to most LDAP servers and clients, it is critical to remember when using this functionality for authentication in your own applications.

The following Perl subroutine will authenticate a given user based on the results of an LDAP bind operation.

 
sub authenticate
{
   my ($uid,$password) = @_;

# Must automatically reject if full
# credentials were not passed.
   if (!$uid || !$password)
   {
      return undef;
   }

   $ldap = new
     Net::LDAPapi("myserver.my.org");

# First, bind anonymously
   if ($ldap->bind_s != LDAP_SUCCESS)
   {
      $ldap->perror("bind_s");
      return undef;
   }

# Now search for $uid in the directory
   if($ldap->search_s("o=My, c=US",
       LDAP_SCOPE_SUBTREE,
       "$USERNAME_ATTR=$uid", ["c"], 1)
           != LDAP_SUCCESS)
   {
      $ldap->perror("search_s");
      return undef;
   }

# Make sure only one entry was
# returned, otherwise $uid was either
# not found or not unique.
   if ($ldap->count_entries != 1)
   {
      $ldap->unbind;
      return undef;
   }

# Set internal pointer to first entry
   if ($ldap->first_entry == 0)
   {
      $ldap->unbind;
      return undef;
   }

# Get the DN of this entry so that we
# can use it as our identity when
# binding.
   if (($dn = $ldap->get_dn) eq "")
   {
      $ldap->unbind;
      return undef;
   }

# Bind to the server using the DN
# found and the password passed to the
# function.
   if ($ldap->bind_s($dn,$password) !=
     LDAP_SUCCESS)
   {
      $ldap->unbind;
      return undef;
   }

# If the bind was successful, we
# unbind and return success.
   $ldap->unbind;
   return 1;
}

The above subroutine takes an arguments for username and password. It then looks up the username in the directory to find a distinguished name associated with the username. Upon a successful search, the script attempts to bind to the server using the distinguished name of the matching entry. If the bind is successful, a 1 is returned. Any failures along the way return an undefined value indicating the user was not properly authenticated.

Note that when designing authentication functions, the denial message should not indicate whether it was the username or password that was incorrect. This ensures that people without direct access to the LDAP server will not be able to simply guess usernames until one is correct.

4.0 Apache and LDAP

Apache can use LDAP to extend its functionality in a number of ways. This section concentrates on exact techniques for integrating LDAP based authentication, access control, URI redirection, and server configuration.

4.1 Authentication and Access Control

The Apache web server has long supported LDAP for authentication and access control.

Two different LDAP modules exist for this purpose. Unfortunately, both of these modules perform authentication in a manner that is both a security risk and non-portable. The exact process involves searching an entry, retrieving its password and checking for a match against the password supplied.

This is bad for a few reasons. First, it requires that the password be stored in a single format, Unix Crypt. This is incompatible with the format used by some LDAP servers. Additionally, the `userPassword' field is normally restricted for reading or searching. The Apache LDAP modules require that this attribute is opened up completely, or restricted to a directory user whose username and password are listed in the Apache configuration files.

The Apache::AuthLDAP module for mod_perl leverages the power of the Net::LDAPapi module to provide extensible LDAP authentication and authorization handlers for Apache.

Apache::AuthLDAP can be found at http://www.wwa.com/~donley/ or the module list at http://perl.apache.org/ . Installation is relatively straight-forward on any system where mod_perl and Net::LDAPapi have already been installed.

The module itself is actually divided into two parts. The first is the AUTH handler, which follows the exact technique described in section 3.4 to authenticate a user based on a given login name and password. The second part is the AUTHZ handler, which analyzes the "require" statements in an Apache access control file to provide access control. It should be noted that mod_perl must have both AUTH and AUTHZ handlers enabled in order to use this module.

The following is an example of an Apache access control file (i.e. access.conf or .htaccess) that will restrict access for a particular area to people who can authenticate as someone with required attribute values.

 
AuthName "Foo Bar Directory"

AuthType Basic

# Set the top level of the tree
PerlSetVar BaseDN "o=Foo,c=US"

# Set name of the LDAP server
PerlSetVar LDAPServer ldap.foo.com

# Set the LDAP Port (optional)
PerlSetVar LDAPPort 389

# Set the UID attribute (def: `uid')
PerlSetVar UIDAttr "uid"

# Set the Authentication Handler to
# the Apache::AuthLDAP module
PerlAuthenHandler Apache::AuthLDAP

# Attribute-based require line
require departmentNumber abc123 abc124

The Apache::AuthLDAP module supports access control based on any of the following:

  • Valid-User
  • User
  • Attribute
  • Groups
  • Filter

Like most other authentication and access control modules, the `valid-user' require line authorizes any user who can successfully authenticate.

User based access controls simply require that the user's login name match one in the require line. For example:

 
require user donley qa1049

would require that the authenticated user have the username `donley' or `qa1049' to be authorized.

Attribute based access controls require that the authenticated user have an attribute with specific values in order to be authorized. An example of this can be found in the full example shown above.

Group based access controls require that the user's distinguished name be in the `member' attribute of a specific entry whose object class designates it as a LDAP group. A group based access control might look something like:

 
require group "cn=ISGroup,o=Foo,c=US"

Finally, filter based access controls allow complex LDAP search filters to be created to control access based on one or more attribute/value combinations. This is currently the only way to implement access controls with multiple conditions. For example, a filter requiring that an authorized user's entry contain a "dept" attribute with the value "abc123" and a surname attribute with a value of "Smith" might look like this:

 
require filter (&(dept=abc123)(sn=Smith))

Since these access controls are implemented in Perl, they are easily extensible by modifying the Apache::AuthLDAP code. No recompilation of the Apache server is necessary after changes. As for performance, the Perl module used by Apache::AuthLDAP to search and bind using LDAP directly interfaces with the same libraries that C-based Apache modules use.

It should be noted that the current implementation of the Apache::AuthLDAP module does a separate LDAP search for each require line. A better implementation might pre-read all of the require lines and do a smaller number of searches. Only one search is made per require line, so the best approach is to only use new require lines when specifying different require types.

4.2 URI Translation

Entries in an LDAP directory may contain attributes with URI values. Such attributes usually contain the home page for the given directory entry. Web servers can make use of these attributes to rewrite URIs based on directory information.

In general, URI rewriting based on LDAP entries can be a good replacement for rewriting based on passwd file entries for sites that distribute users' personal web pages across multiple servers, but don't want to interconnect them using a network filesystem or other means. For example, one might automatically redirect all queries to `~username' on a web server to a URI residing in the LDAP entry for `username'.

One way to add this functionality to Apache would be to develop a module similar to the `mod_userdir' bundled with Apache. Such a module would simply parse an existing URI, substituting the results from an LDAP query a necessary.

Another way to add this functionality would be to have a CGI interface perform the URI rewriting. This is most useful for complex redirections and will be described further in section 5.2.

Apache::TransLDAP is a reference implementation of a mod_perl Apache module that will rewrite URIs based on LDAP attributes. It can be found at http://www.wwa.com/~donley/ and the Apache module list at http://perl.apache.org/ .

The following is a short example of the use of this module to rewrite URIs from /users/username on the current server to the labeledURI attribute in username's LDAP entry.

 
# Set the TransHandler to be TransLDAP
PerlTransHandler Apache::TransLDAP

# Set Server and Search Information
PerlSetVar  LDAPServer ldap.my.org
PerlSetVar  LDAPBase "o=My,c=US"
PerlSetVar  UIDAttr "uid"

# This is the standard attribute for
# URIs within an LDAP entry
PerlSetVar  URIAttr "labeleduri"

# The Virutal Home Page URI
PerlSetVar  UserDir "/users/"

With this module enabled, each request to the Apache server will call the Apache::TransLDAP module as a Perl TRANS handler. In order to work, TRANS handlers must be enabled in mod_perl and Net::LDAPapi must already be installed.

For each request, the module will first check to see if the URI begins with UserDir. If not, it declines to make changes and passes control back to Apache. Otherwise, a connection is opened to the LDAP server to find the entry for the specified user.

If an entry is not found, or the entry does not contain a value for the required URI attribute, the module will pass control back to Apache without making changes. Upon success, the initial part of the URI is translated to the LDAP attribute's value. This new URI is then returned to the web client as a redirect, response code 301.

While the above technique works well for redirecting based on attributes within users, it can also be used to create virtual home-pages for organizations, groups, and other object classes within the directory.

4.3 Server Configuration

One of the more powerful features of Apache and mod_perl is the ability to have Perl based configuration sections within httpd.conf and other server configuration files.

Combined with the Net::LDAPapi module, the Apache server has the ability to obtain configuration information from an LDAP server. This is most useful in using an LDAP directory to control configuration for a cluster of servers.

Prior to actually configuring a server via LDAP and Perl, it is first necessary to create an object class on the directory server that contains the configuration attributes needed by the server. For our example, the following attributes might make up an object class used for location access controls.

 
locationURI
authType
authName
require
validClusterNumber

Once the object class is created on the server, entries might be populated in the directory with information like:

 
objectClass: apacheACL
location: /protected
authType: basic
authName: Protected
require: valid-user

objectClass: apacheACL
location: /private
authType: basic
authName: Private
require: user joe mary sam
require: group humans

These LDAP entries could then be read using a <Perl> section within the Apache configuration files. Such a section might look as follows.

 
# Create a new connection and bind
my $ldap = new Net::LDAPapi($server);
$ldap->bind_s;

# Search the directory for objects of
# the type I'm looking for.
if ($ldap->search_s($BASE, LDAP_SCOPE_SUBTREE, "objectclass=apacheACL", [], 0) == LDAP_SUCCESS)
{
# Get all returned entries.
   my $locations =
       $ldap->get_all_entries;

# Go through each entry returned and
# create an access control for that
# location based on information in the
# directory.
 foreach my $dn (keys %{$locations})
 {
   my %entry = %{$locations->{$dn}};

   # Here we actually build the ACL
   $Location{$entry{"location"}->[0]}=
   {
    AuthType=>$entry{"authType"}->[0],
    AuthName=>$entry{"authName"}->[0],
    Limit => {
      METHODS=> `GET POST',
      require=> [@$entry{"require"}],
    }
   };
 }
}

The segment featured above would allow the web server to retrieve all access control information from the directory server. Multiple servers using the same code segment would have identical access controls, while localized changes could also be added to local configuration files without affecting global controls.

The object class schema used for this example could be extended to include a cluster identifier, thus allowing different clusters to read different configurations. The only part of the code needing to change to support such functionality would be the search. This could also be done by having each cluster configured within different parts of the directory tree. In this case, the $BASE in the above example would be changed to correspond with the correct part of the tree.

5.0 Common Gateway Interfaces (CGI)

Like most Perl modules, the power of Net::LDAPapi is enhanced by combining use with other modules. This section details some of the ways that Net::LDAPapi can be used along-side the CGI module to extend or create web applications.

5.1 Personalization

A side benefit of authenticating users is that they can be offered personalized content based on information stored in their LDAP entry. This personalization can be performed in a couple of ways. The first possibility would be to have the CGI script offer different functionality or information based on group membership or existing attribute values.

A more powerful method would be to create a new LDAP object class that contains configuration-based attributes. Entries can then be made members of this new object class and populated with personal preferences or configuration information. This creates "roaming profiles" that can be accessed and changed using the LDAP standard.

The following example shows how data within an LDAP entry can be used to personalize the information sent by a CGI.

 
# Set $username to the authenticated
# username as determined by the server
$username = remote_user;

# A reference to a list of attributes
# to be returned by the LDAP server
$configattrs = [
   "favoriteColor",
   "age",
   "license",
];

# Open a new connection and bind
$ldap = new Net::LDAPapi($myserver);
$ldap->bind_s;

# Search the directory
if ($ldap->search_s($BASE, LDAP_SCOPE_SUBTREE, "uid=$username", $configattrs, 0) != LDAP_SUCCESS)
{
   $ldap->perror("search_s");
   $ldap->unbind;
   die;
}

# Advance to the first entry
$ldap->first_entry;

# Get the values for our config
# attributes
@color = $ldap-> get_values("favoritecolor");
@age = $ldap->get_values("age");
@license = $ldap->get_values("license");

$ldap->unbind;

print header;
print title("Hello, $username"),
         h1("Hello, $username");

# Print the first item in @color
print "You like " . $color[0],p;

# Print conditional text based on age
if ($age[0] < 18)
{
   print "You are too young to see.";
} else {
   print "There is nothing to see.";
}

# Determine if a user is licensed
# for this feature.
if (grep("MySoft",@license))
{
   print p,"You are licensed.";
}

The only drawback to the implementation of LDAP-based preferences is that adding object classes to an LDAP server is not always a trivial task. To get around this, LDAPv3 adds an object class called `extensibleObject' that allows arbitrary pairs of attributes and values. Some LDAPv2 servers have an option to disable schema checking which provides this functionality at the server level, rather than at the entry level as the `extensibleObject' object class permits.

5.2 Redirection

Another use for LDAP in a CGI application would be to redirect authenticated users to specific web-sites based on values within an entry. For example, one could find an entry for the authenticated user's department and automatically go to a URI based on that information.

An example of a CGI to do URI redirection based on LDAP attribute values. A good use for this would be as the default home page for internal users that would automatically change to their personal home page or their department's home page if they exist.

 
# Set the username as returned by the
# web server
$username = remote_user;

# Reference to a list containing the
# attributes to be used for the
# redirect
$attrs = [
   "labeleduri",
   "department",
];

# Create the connection and bind
$ldap = new Net::LDAPapi($myserver);
$ldap->bind_s;

# Search the directory
if ($ldap->search_s($BASE, LDAP_SCOPE_SUBTREE, "uid=$username",
 $configattrs, 0) != LDAP_SUCCESS)
{
   $ldap->perror("search_s");
   $ldap->unbind;
   die;
}

# Advance to the first entry.
$ldap->first_entry;

# Obtain the values for this entry
@labeleduri =
  $ldap->get_values("labeleduri");
@dp = $ldap->get_values("department");

$ldap->unbind;

# If a LabeledURI is found, redirect
# to that page, otherwise redirect to
# the department home page.
if ($#labeleduri < 0)
{
   if ($#department < 0)
   {
      print
        redirect($default_home_page);
   } else {
      print redirect("/" .
        $department[0]);
   }
} else {
   print redirect($labeleduri[0]);
}

5.3 Directory Updates

When using a directory as a source of user and server configuration data, it is often necessary for the CGI script to update data within the directory. This may be performed either explicitly when requested by the user (i.e. change a favorite color) or behind the scenes (i.e. update the number of logins).

Many approaches to this would bind to the directory as a user with privileges to modify all user entries. Security is then controlled by the application. Unfortunately, this requires a high level of trust when multiple developers use the same directory.

To get around this, it is necessary to obtain the directory login credentials within the CGI, rather than retrieve the authenticated username from the web server. These credentials can in turn be passed to the directory server using the "bind_s" method prior to making a directory update.

The authentication technique is the same as the one described in section 3.4, except that the script would not unbind from the server after authentication. An example of doing this can be found in the "web500.pl" example included in the Net::LDAPapi package.

6.0 Security

There are a number of issues to consider when deploying applications and services with LDAP interfaces. This section discusses issues surrounding over-the-wire encryption and using a directory for single sign-on.

6.1 Encrypted Passwords and Data

While most modern LDAP servers encrypt passwords on disk, the LDAPv2 specification defines no mechanism for sending encrypted passwords or data over the network. This means that a directory is only as secure as its network link.

For a small switched intranet, this might be sufficient. Unfortunately, this is hardly the case when sending sensitive passwords and data over the Internet. To get around this issue, a number of vendors have provided support for the Secure Socket Layer (SSL), but this is not a part of the standard and is not supported by any free implementations.

The LDAPv3 standard addresses this issue in two ways. First, it supports Transport Layer Security (TLS), the next generation of SSL. This is useful for encrypting an entire session between LDAP clients and servers. Additionally, the Simple Authentication and Security Layer (SASL) is supported, thus allowing clients and servers to authenticate one other, as well as negotiate session encryption.

6.2 Single Sign-On

In Intranet environments, LDAP is often viewed with an eye towards single sign-on. This is because LDAP is supported for the publication of digital certificates, which often make extremely secure single sign-on possible.

The authentication mechanisms described earlier in this document could be used to develop a login/password based single sign-on system. Such a system has many advantages over traditional authentication sources in such areas as account auditing and maintenance. While these advantages are nice, it is important to consider the additional risks created by such a system.

Any password can be compromised, whether through technical means or social engineering.. In a typical environment, a password only unlocks access to a single system or group of systems maintained by the same administrative group. Single sign-on adds risk to this in two ways.

First, the password used is more likely to go over "foreign" wires to "foreign" systems where there is a higher possibility of being captured by a sniffer or malicious application. Second, once compromised, the password is likely to unlock more doors.

These added risks can be minimalized by the encryption methods mentioned in the previous section. Additional care needs to be taken to insure that CGI developers are not storing clear-text versions of entered passwords on disk or sending them over the network.

7.0 Performance Tuning

While it is beyond the scope of this document to describe exact techniques for increasing performance in specific directory products, there are a number of general areas that can be looked at when shooting for higher performance in directory enabled applications.

The most common issue people have with LDAP servers is typically that some search filters return results extremely fast, while other search filters will take forever, time-out, or hit resource limits on the same server. This is almost always an indexing issue.

Virtually all LDAP servers have settings that specify which attributes are indexed or hashed. Adding indexes typically increase disk usage and update time, but will likely be the only way that searches can be completed in a timely manner on larger directories.

For speed, most LDAP servers cache recently retrieved entries. The number of entries allowed in the cache is usually tunable, as is the maximum size of the cache itself. Setting the maximum size too large might cause the machine to run out of memory and begin swapping to disk, causing overall poor system performance. Setting the cache too small is not likely to severely affect servers with good disk based storage schemes, but could be disastrous on servers that basically require all entries to be read into memory for decent performance.

Performance issues beyond the ones listed above do exist, but are mostly implementation specific.

8.0 LDAP Resources

While not a comprehensive list of LDAP-related sites, this section lists a number of good on-line resources for additional information about LDAP.

A good starting point for understanding more about directory services is Jeff Hodges' "LDAP Roadmap and FAQ" at http://www.kingsmountain.com/ldapRoadmap.shtml . Jeff has filled this page with a wide variety of up-to-date information and links related to LDAP, X.500, and directory services in general.

While somewhat outdated, the University of Michigan's LDAP page at http://www.umich.edu/~dirsvcs/ldap/ contains pointers to other LDAP resources, as well as source to the only freely available LDAP server at the time of this writing.

Innosoft, the creators of an LDAP proxy server, as well as other goodies, has a page dedicated to LDAP at http://www.critical-angle.com/ldapworld/ . Patches for the University of Michigan's LDAP server can be found here as well.

Netscape has a freely available C and Java software developers' kit at http://developer.netscape.com/tech/directory/ . The source for these kits can be found at http://www.mozilla.org/ldap/ .

The author's site at http://www.wwa.com/~donley/ contains information and source code that enables LDAP in Perl, Apache, Cyrus IMAPd, and the Squid web proxy.

Graham Barr has a page at http://www.connect.net/gbarr/ dedicated to a completely Perl LDAP developers' kit. An ambitious project, it also includes a BER encoding/decoding module that could be used as a baseline for developing an all-Perl LDAP server.

Finally, one of the best places to ask questions regarding LDAP is the ldap@umich.edu mailing list. Information on joining it can be found on the University of Michigan web site listed previously. An archive of list postings is kept on http://www.reference.com/ .

9.0 Glossary

API
Short for "Application Program Interface". Functions or class methods that can be used by programmers to simplify the addition of a set of functionality.
Abstract Syntax Notation One (ASN.1)
A language for defining abstract data structures. Defined in ITU-T X.208.
Basic Encoding Rules (BER)
A specification for encoding an ASN.1-defined data structure for transmission over a data connection. Defined in ITU-T X.209.
CPAN
Comprehensive Perl Archive Network. Sites throughout the world that carry Perl sources, as well as Perl examples and modules. Can be found by referencing http://www.perl.com/CPAN-local/.
Directory Access Protocol (DAP)
An OSI standard for accessing X.500 directory services.
Distinguished Name (DN)
The fully qualified name of a directory entry. It specifies both an entry's location in the directory tree and its name.
International Telecommunications Union (ITU)
The standards organization that released the X.500 specifications.
Lightweight Directory Access Protocol (LDAP)
An Internet standard for accessing directory services. A subset of DAP that runs over a TCP socket.
LDAPv2
Version 2 of the LDAP. Previous version of the standard supported by a large number of clients and servers, including those freely available from the University of Michigan. Defined in RFCs 1777-1779
LDAPv3
Version 3 of the LDAP. Latest proposed version of the standard. Defined in RFCs 2251-2256.
Perl
Per the Camel Book, the "Pathologically Eclectic Rubbish Lister", or perhaps the "Practical Extraction and Report Language". One of the reasons you attended this conference.
Relative Distinguished Name (RDN)
A component of the distinguished name consisting of at least one attribute/value pair.
Simple Authentication and Security Layer (SASL)
Extensible mechanism for authenticating clients and servers. Also allows for the negotiation of session encryption. Defined in RFC 2222.
Transport Level Security (TLS)
The next generation of SSL. Useful for encrypting session tranmissions. Also useful as an external SASL method. Currently an Internet Draft.
X.500
The OSI directory standard defined in ITU-T X.500.
XS
A language used to write extensions in Perl that interface with C functions. Compiled into C code by `xsubpp'.