LDAP, the hard way

Well, its a new year, and therefore its an appropriate time to begin my journey into the world of LDAP and Identity Management. I'd like to start by discussing some of the recent history of LDAP on .NET (dating no further back than .NET 1.1), and then I'll move ahead into an exploration of .NET 2.0's System.DirectoryServices.Protocols namespace. The classes in this namespace are new to me, and are probably new to you too. I'll likely stumble a few times along the way (please hold me to account if you find any errors), but I hope I'll illustrate how these classes provide you with secure, scalable and high-performance means of accessing LDAP directories.

"Why not just use System.DirectoryServices"?

Let me start by saying, yes, you absolutely can access LDAP directories via System.DirectoryServices. On .NET 1.x, these classes provided a nice managed wrapper over the COM-based ADSI libraries, and were well-documented and well-supported for writing applications that accessed directories. This worked great for applications that didn't need to support a large number of distinct identities accessed on a larger number of distinct threads. Unfortunately, if you dropped code that into an assembly loaded by IIS and authenticated a large number (>1000s) of users through it, you'd find that your app's responsiveness would drop quite a bit.

After a lot of head scratching and running some controlled experiments with some very bright colleagues of mine, we discovered why. We were running out of available TCP sockets, and we discovered that the more distinct identities we randomized through the app, the quicker we'd get into trouble. To cut right to it, it turns out that the reason this happened was by design, namely, ADSI (and thus System.DirectoryServices) weren't intended to authenticate 1000s of identities in the middle-tier. Unfortunately, enterprise developers sometimes have to connect LDAP directories other than Active Directory, and sometimes they have a large number of users to bind to those LDAP directories. So how did we resolve it?

WLDAP32.DLL to the rescue

In reviewing a few memory dumps, we discovered a library that was loaded under System.DirectoryServices and ADSI on every thread with a hung up socket. That library was WLDAP32.DLL, and it was the key to our getting ripping fast LDAP binds going on .NET 1.x. Unfortunately, WLDAP32.DLL isn't very well-known or well-documented - at least not via P/Invoke from C#. There isn't even a list of declarations for it up on http://www.pinvoke.net. That lead to many more hours of head scratching, and some creative use of a C++ shim DLL to figure out how to get my marshaling to/from unmanaged memory right. I don't envy anyone attempting to do this, and I sincerely hope that if you're ever faced with this issue, you'll upgrade to .NET 2.0 and bypass the whole thing.  But this does illustrate what's happening at the lower API levels when you authenticate users via LDAP binds. I'm including this here with the hope that it will shed some light on a topic I found to be rather murky prior to going through this process.

Init, bind, and bind again

To connect to an LDAP directory via WLDAP32.DLL, you need to call the ldap_init() or ldap_sslinit() function. I prefer ldap_sslinit() here because I can make it behave just like ldap_init() by changing the value of a single argument. Depending on the value of said argument, the ldap_sslinit() function will establish a clear-text LDAP connection or a TLS protected LDAP connection (by default on ports 389 and 636 respectively.) Their managed code declarations look like this:

[ComVisible(false), SuppressUnmanagedCodeSecurityAttribute()]
internal class WLdap32
/// <summary>
/// The ldap_init function initializes a session with an LDAP server.
/// </summary>
/// <see cref="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ldap/ldap/ldap_init.asp?frame=true"/>
[DllImport("wldap32.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "ldap_initW", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr ldap_init(string hostName, uint portNumber);

  /// <summary>
/// The ldap_sslinit function initializes a Secure Sockets Layer (SSL) session with an LDAP server.
/// </summary>
/// <param name="hostName"></param>
/// <param name="portNumber"></param>
/// <param name="secure"></param>
/// <returns></returns>
/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ldap/ldap/ldap_sslinit.asp?frame=true
[DllImport("wldap32.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "ldap_sslinitW", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern IntPtr ldap_sslinit(string hostName, uint portNumber, int secure);

Both these functions return an IntPtr, which you'll need to hang on to for every subsequent call you make until you close the connection. Once you have such an IntPtr, authenticating a user is a matter of binding the LDAP directory. There are many ways to do this, but for enterprise environments where only port 389 or 636 connections are allowed, the ldap_simple_bind_s() function is the simplest to implement and the easiest to understand. Its manged code declaration looks like this:

[ComVisible(false), SuppressUnmanagedCodeSecurityAttribute()]
internal class WLdap32
// other declarations, then...

  /// <summary>
/// The ldap_simple_bind_s function synchronously authenticates a client to server, using a plaintext password.
/// </summary>
/// <param name="ldapHandle"></param>
/// <param name="distinguishedName"></param>
/// <param name="password">Password of user in Active Directory</param>
/// <returns></returns>
/// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/ldap/ldap/ldap_simple_bind_s.asp
  [DllImport("wldap32.dll", CallingConvention = CallingConvention.Cdecl, EntryPoint = "ldap_simple_bind_sW", CharSet = CharSet.Unicode)]
public static extern int ldap_simple_bind_s([In] IntPtr ldapHandle, string distinguishedName, string password);

A couple things to note here:

  • As the <summary> indicates, ldap_simple_bind_s() sends passwords to the server in plaintext. I do not recommend using it over anything other than a secure LDAP connection (or over one with a transport secured via other means.)
  • The second argument is the distinguishedName of the user (i.e., not the username.) The username you type into the Windows Login dialog corresponds to Active Directory's sAMAccountName attribute - you'll need to distinguishedName associated with that sAMAccountName in order to authenticate via ldap_simple_bind_s(), and you should also note that you often can't simply derive the distingusihedName from the username (at least not on Windows.)

Have I dissauded you from attempting this yet?

Boy I sure hope so, but if you really insist on trying, the code you'll need to call these functions wil look something like this:

// standard console stuff and namespace declarations, then...
void AuthenticateViaLdap(string userDn, string password)
// read "Writing Secure Code", then *never* write code like this,
// but for sake of illustration...
string host = "somehost.dns.name";
int port = 636; // the default LDAPS port
int secure = Convert.ToInt32(true);
IntPtr pCnx = WLdap32.ldap_sslinit(host, port, secure);
int result = WLdap32.ldap_simple_bind_s(pCnx, userDn, password);
// hope you get back result == 0

As you can guess, a result of 0 indicates that the user was successfully authenticated. In my next post I'll explore what happens when you get a non-zero result back from this function and things you might want to do about that.

Happy New Year!