Getting Started with the LDAP Component


Requirements: IPWorks

The Lightweight Directory Access Protocol (LDAP) is the standard protocol for accessing directory servers, such as Microsoft Active Directory, OpenLDAP, and 389 Directory Server. Directory servers are commonly used to store user accounts, groups, and other information about the people and computers in an organization, and to authenticate users against that information.

This article covers how to use the LDAP component to authenticate users, search the directory, and manage the entries in it.

Contents

Connecting and Binding

Every entry in an LDAP directory is identified by its distinguished name (DN), such as CN=John Smith,CN=Users,DC=example,DC=com. Before performing most operations, the component must bind (log in) to the server. To do so, set the ServerName property to the name of the server, set the DN and Password properties to the credentials of an account in the directory, and call the Bind method.

LDAP ldap = new LDAP();
ldap.ServerName = "ldap.example.com";
ldap.ServerPort = 636;
ldap.SSLStartMode = LDAPSSLStartModes.sslImplicit;
// A timeout greater than 0 makes the component's methods block until the operation completes.
ldap.Timeout = 30;
ldap.DN = "CN=Service Account,CN=Users,DC=example,DC=com";
ldap.Password = "PASSWORD";
ldap.Bind();
if (ldap.ResultCode != 0) {
  Console.WriteLine("Bind failed: " + ldap.ResultDescription);
}

When the server returns an error for an operation (such as invalid credentials), the component does not throw an exception. Instead, the result of each operation is reported in the ResultCode and ResultDescription properties (and through the Result event), so these should be checked after each call. A ResultCode of 0 indicates success.

Since a simple bind sends the password to the server as it is, the connection should be secured with TLS. The example above connects to the LDAPS port (636) with SSLStartMode set to sslImplicit. Alternatively, to connect to the standard LDAP port (389) and then upgrade the connection with StartTLS, set SSLStartMode to sslExplicit.

Active Directory also accepts other forms of the user name in place of a DN, such as EXAMPLE\jsmith or jsmith@example.com. On Windows, setting the AuthMechanism property to lamNegotiate or lamKerberos authenticates using NTLM or Kerberos instead of a simple bind.

Authenticating Users

A common use of LDAP is verifying the user name and password entered into a login form. Users usually log in with a short user name, rather than the DN of their entry, so the DN is found first by searching the directory, and the user's password is then checked by binding with that DN. The attribute that holds the user name depends on the server, and is typically sAMAccountName in Active Directory, or uid in OpenLDAP and other directories.

string FindUserDN(LDAP ldap, string baseDN, string userName) {
  string userDN = "";
  ldap.OnSearchResult += (s, e) => { userDN = e.DN; };
  ldap.DN = baseDN;
  ldap.SearchScope = LDAPSearchScopes.ssWholeSubtree;
  ldap.SearchSizeLimit = 2;
  ldap.Search("(sAMAccountName=" + EscapeFilterValue(userName) + ")");
  return userDN;
}
bool Authenticate(string userName, string password) {
  // An empty password results in an anonymous bind, which most servers allow, so it must be rejected.
  if (userName == "" || password == "") return false;
  LDAP ldap = new LDAP();
  ldap.ServerName = "ldap.example.com";
  ldap.ServerPort = 636;
  ldap.SSLStartMode = LDAPSSLStartModes.sslImplicit;
  ldap.Timeout = 30;
  // Bind with a service account that is allowed to search the directory.
  ldap.DN = "CN=Service Account,CN=Users,DC=example,DC=com";
  ldap.Password = "PASSWORD";
  ldap.Bind();
  if (ldap.ResultCode != 0) return false;
  string userDN = FindUserDN(ldap, "DC=example,DC=com", userName);
  ldap.Unbind();
  if (userDN == "") return false; // The user was not found.
  // Bind again as the user to check the password.
  ldap.DN = userDN;
  ldap.Password = password;
  ldap.Bind();
  bool authenticated = (ldap.ResultCode == 0);
  ldap.Unbind();
  return authenticated;
}

If the password is incorrect, the bind fails with result code 49 (invalidCredentials).

The user name is inserted into a search filter, so any characters that have a special meaning in filters must be escaped first. Otherwise, a user name such as * would match every entry in the directory. RFC 4515 specifies that these characters are replaced with a backslash followed by their hexadecimal value:

string EscapeFilterValue(string value) {
  StringBuilder sb = new StringBuilder();
  foreach (char c in value) {
    switch (c) {
      case '\\': sb.Append("\\5c"); break;
      case '*': sb.Append("\\2a"); break;
      case '(': sb.Append("\\28"); break;
      case ')': sb.Append("\\29"); break;
      case '\0': sb.Append("\\00"); break;
      default: sb.Append(c); break;
    }
  }
  return sb.ToString();
}

Finding the Base DN

Searches start from a base DN, which is usually the top of the directory tree (for example, DC=example,DC=com). When the base DN is not known in advance, it can be retrieved from the server's root DSE, which is a special entry that contains information about the server itself. The root DSE is retrieved by searching with an empty DN, a SearchScope of ssBaseObject, and the filter objectClass=*.

ldap.DN = "";
ldap.SearchScope = LDAPSearchScopes.ssBaseObject;
ldap.Search("objectClass=*");
// In Active Directory, the defaultNamingContext attribute holds the DN of the domain.
string baseDN = ldap.Attr("defaultNamingContext");

Other directories may not return defaultNamingContext, but all LDAP v3 servers return the namingContexts attribute, which lists the top-level DN of each directory tree held by the server. A server may hold several trees (for example, one for employees and one for customers), so this attribute often has more than one value.

In the Attributes collection, an attribute with several values is stored as one item per value. The first item holds the attribute's name in its AttributeType field, and the items that follow it have an empty AttributeType, so they belong to the same attribute:

string currentType = "";
foreach (LDAPAttribute attr in ldap.Attributes) {
  if (attr.AttributeType != "") currentType = attr.AttributeType;
  if (currentType == "namingContexts") {
    Console.WriteLine("Naming context: " + attr.Value);
  }
}

For more information about the root DSE, please see our Retrieving Server Information from the Root DSE with the LDAP Component entry.

Searching the Directory

The Search method searches the directory, starting from the entry specified by the DN property, for the entries that match a search filter. The SearchScope property determines how much of the tree is searched: ssBaseObject searches only the base entry, ssSingleLevel searches its immediate children, and ssWholeSubtree (the default) searches the entire tree below it.

The SearchResult event fires once for each matching entry, with the DN of the entry in its DN parameter. During this event, the Attributes collection holds the attributes of that entry. After all of the results have been returned, the SearchComplete event fires.

ldap.OnSearchResult += (s, e) => {
  Console.WriteLine("Entry: " + e.DN);
  foreach (LDAPAttribute attr in ldap.Attributes) {
    Console.WriteLine("  " + attr.AttributeType + ": " + attr.Value);
  }
};
ldap.DN = "OU=Staff,DC=example,DC=com";
ldap.SearchScope = LDAPSearchScopes.ssWholeSubtree;
// Return only the mail and telephoneNumber attributes of each entry.
ldap.Attributes.Clear();
ldap.Attributes.Add(new LDAPAttribute("mail"));
ldap.Attributes.Add(new LDAPAttribute("telephoneNumber"));
ldap.Search("(&(objectClass=user)(department=Sales))");

When the Attributes collection is empty, the server returns all of the entry's attributes. Other properties control the search further:

  • SearchSizeLimit and SearchTimeLimit limit the number of entries returned and the time the server spends on the search.
  • SearchReturnValues can be set to false to return only the attribute names, without their values.
  • PageSize enables paged results, which is required to retrieve more entries than the server's size limit (1000 entries by default in Active Directory).
  • SortAttributes requests that the server sort the results by the specified attributes.

Adding Entries

To add an entry, set the DN property to the DN of the new entry, add its attributes to the Attributes collection, and call the Add method. The DN is formed by adding the entry's relative name (such as uid=jsmith or CN=John Smith) to the DN of the container that will hold it.

Each entry must have the attributes required by its object classes, which are defined in the server's schema. The example below adds a user to an OpenLDAP directory using the standard inetOrgPerson object class, which requires the cn and sn attributes. Since objectClass has several values, the values after the first are added with an empty attribute type.

ldap.DN = "uid=jsmith,ou=people,dc=example,dc=com";
ldap.Attributes.Clear();
ldap.Attributes.Add(new LDAPAttribute("objectClass", "top"));
ldap.Attributes.Add(new LDAPAttribute("", "person"));
ldap.Attributes.Add(new LDAPAttribute("", "organizationalPerson"));
ldap.Attributes.Add(new LDAPAttribute("", "inetOrgPerson"));
ldap.Attributes.Add(new LDAPAttribute("uid", "jsmith"));
ldap.Attributes.Add(new LDAPAttribute("cn", "John Smith"));
ldap.Attributes.Add(new LDAPAttribute("sn", "Smith"));
ldap.Attributes.Add(new LDAPAttribute("mail", "jsmith@example.com"));
ldap.Attributes.Add(new LDAPAttribute("userPassword", "InitialPassword1!"));
ldap.Add();
if (ldap.ResultCode != 0) {
  Console.WriteLine("Add failed: " + ldap.ResultDescription);
}

User accounts in Active Directory are created differently:

  • The entry uses the user object class, and must include the sAMAccountName attribute (the user's login name). The userPrincipalName attribute (such as jsmith@example.com) is also commonly set.
  • New accounts are disabled by default. The password is set through the unicodePwd attribute (over a secure connection), and the account is then enabled by setting the userAccountControl attribute to 512 (a normal, enabled account).

For more information about setting passwords, please see our Changing User Passwords with the LDAP Component entry.

Modifying Entries

To change an existing entry, set the DN property to the DN of the entry, add the changes to the Attributes collection, and call the Modify method. The ModOp field of each attribute specifies the change to make:

  • amoAdd adds the specified values to the attribute.
  • amoDelete deletes the specified values from the attribute, or the entire attribute when no value is specified.
  • amoReplace replaces all of the attribute's values with the specified values.
ldap.DN = "uid=jsmith,ou=people,dc=example,dc=com";
ldap.Attributes.Clear();
// Several changes can be made to the same entry in one request.
ldap.Attributes.Add(new LDAPAttribute("telephoneNumber", "+1 919 555 0100", LDAPAttributeModOps.amoReplace));
ldap.Attributes.Add(new LDAPAttribute("description", "Sales team lead", LDAPAttributeModOps.amoReplace));
ldap.Attributes.Add(new LDAPAttribute("mobile", "", LDAPAttributeModOps.amoDelete));
ldap.Modify();

The server applies all of the changes in a single request, so either all of them succeed, or none of them do.

To rename an entry, use the ModifyRDN method, and to move an entry to a different container, use the MoveToDN method.

Deleting Entries

To delete an entry, set the DN property to the DN of the entry and call the Delete method.

ldap.DN = "uid=jsmith,ou=people,dc=example,dc=com";
ldap.Delete();

Most servers only allow entries without children to be deleted, so the entries below a container must be deleted before the container itself.

Rather than deleting a user's entry, it is often better to disable the account, so that a record of it is kept. In Active Directory, an account is disabled by setting the ACCOUNTDISABLE flag (2) of its userAccountControl attribute (e.g., by replacing the value 512 with 514). Other directories provide their own mechanisms for this, such as password policy attributes, so please refer to the server's documentation.

Active Directory Helper Methods

The LDAP component also includes several methods that perform common Active Directory tasks without requiring a search filter to be built:

Method Description
ListGroups Lists all of the groups in the domain, firing the GroupList event for each group.
ListGroupMembers Lists the members of the specified group, firing the UserList event for each member.
ListUserGroups Lists the groups the specified user belongs to, firing the GroupList event for each group.
ListComputers Lists the computers in the domain, firing the ComputerList event for each computer.
ChangePassword Changes or resets the password of the specified user.
ldap.OnGroupList += (s, e) => {
  Console.WriteLine("Group: " + e.name + " (" + e.dn + ")");
};
// The user is specified by the common name (CN) of their entry.
ldap.ListUserGroups("John Smith");

These methods locate the domain's Users and Computers containers automatically using the root DSE. If the entries are stored in a different container or organizational unit, set the UsersDN or ComputersDN configuration setting to its DN.

Notes

  • The Bind method establishes the connection to the server, so it must be called before other operations such as Search and Modify. For servers that allow anonymous access, the Connect method can be used instead, which connects to the server without binding.
  • When Timeout is set to 0, the component operates asynchronously, and the results are reported through events, such as Result and SearchComplete. The Active Directory helper methods require a Timeout greater than 0.
  • The server's certificate is checked during the TLS handshake. To accept a certificate that is not trusted by the system (such as a self-signed certificate on a test server), handle the SSLServerAuthentication event.
  • Binary attributes, such as objectGUID and objectSid in Active Directory, are available through the ValueB field of each attribute. The FriendlyGUID and FriendlySID configuration settings convert these values to readable strings.

Note that in older versions of IPWorks, attributes were specified and read using the AttrCount, AttrType, AttrValue, and AttrModOp array properties, which have been replaced by the Attributes collection.

We appreciate your feedback. If you have any questions, comments, or suggestions about this article please contact our support team at support@nsoftware.com.