Friday, January 11, 2008

eDirectory Authentication using LdapConnection and custom certificate validation

Introduction
This article explains how to authenticate a user over LDAPS using the System.DirectoryServices.Protocols.LdapConnection class, performing custom certificate validation.

Background
Recently, I ran into trouble using System.DirectoryServices.DirectoryEntry to connect to a Novell eDirectory server because the certificate was self-signed. When run in an ASP.NET application, the machine-level certificate store was not examined. So, even though the self-signed certificate was in the trusted store, DirectoryEntry was still refusing to establish a connection.

The LdapConnection class is a better choice for this situation, as it allows the user to validate the certificate manually. Note that the DirectoryEntry approach works fine with a trusted self-signed certificate when run in a Windows Forms application. A good example of using DirectoryEntry to connect to eDirectory can be found here.

Using the code
The example uses a Login control for simplicity. I recommend that in the "real world" you create a custom MembershipProvider. The following example code was used to connect to a Novell eDirectory server over secure LDAP. However, the code should work fine with other directory providers as long as the correct server/port/root DN is used.

Also remember to omit the con.SessionOptions.SecureSocketLayer = true line if you aren't using LDAPS. However, if you aren't using LDAPS you might as well use DirectoryEntry!

Connecting and authenticating
First, we set up our LdapConnection class. We specify the address and port of the server that we will be connecting over SSL, set up the certificate callback (more on that later) and provide the default credentials for authentication.

This example uses eDirectory's "contextless login" feature, so blank credentials are allowed. Depending on your LDAP server, you may need to specify credentials to search the directory. Also note that if no port is specified, then the default value of 389 will be used.

LdapConnection con = new LdapConnection(new LdapDirectoryIdentifier(
"EDIRECTORYSERVER:636"));
con.SessionOptions.SecureSocketLayer = true;
con.SessionOptions.VerifyServerCertificate =
new VerifyServerCertificateCallback(ServerCallback);
con.Credential = new NetworkCredential(String.Empty, String.Empty);
con.AuthType = AuthType.Basic;

Now we bind the initial connection. If con.Bind() executes without throwing an exception, the server and credentials specified are valid.

using (con)
{
con.Bind();

The next step is to search for the user's fully qualified, distinguished name. This is a necessary step because when users provide their usernames, they don't provide the full context of their names in the directory; i.e. jlennon is provided instead of cn=jlennon,ou=Beatles,ou=Artists,o=AppleRecordsLDAPDirectory.

First, we prepare the SearchRequest object. We specify the root DN, the search filter and the search scope. Then we send the request.

SearchRequest request = new SearchRequest(
"o=LDAPRoot",
"(&(objectClass=Person)(uid=" + Login1.UserName + "))",
SearchScope.Subtree);

SearchResponse response = (SearchResponse)con.SendRequest(request);

If we get this far without an exception being thrown, we know that the root DN and search filter specified are valid. If either is invalid, a DirectoryOperationException will be thrown.

Now we can extract the DN from the search result. If you want to provide a "no such username" message, you can check that response.Entries.Count > 0. An ArgumentOutOfRangeException will be thrown on the following line if the Username provided does not exist.

SearchResultEntry entry = response.Entries[0];
string dn = entry.DistinguishedName;

Now that we have the full DN for the user, we can check if the given password is valid. We set a new NetworkConnection on the LdapConnection object and re-bind. con.Bind() will throw LdapException if the password provided is invalid.

con.Credential = new NetworkCredential(dn, Login1.Password);
con.Bind();
}

If we get this far, we have successfully authenticated! We can now use a SearchRequest to search for group membership, etc. However, that's out of scope for this example.

VerifyServerCertificateCallback
This should need no explanation. We simply load the certificate file from disk and compare it to the certificate presented by the server. Production code should handle exceptions associated with reading the certificate: File not found, access denied, etc. If you really trust the server, you could omit all of this and just return true. ;)

public static bool ServerCallback(
LdapConnection connection, X509Certificate certificate)
{
try
{
X509Certificate expectedCert =
X509Certificate.CreateFromCertFile(
"C:\\certificates\\certificate.cer");

if (expectedCert.Equals(certificate))
{
return true;
}
else
{
return false;
}
}
catch (Exception ex)
{
return false;
}
}

No comments: