Authenticating a user over LDAP in .Net: LdapConnection vs. PrincipalContext

Background: I needed to authenticate users coming to an ASP.NET site against an LDAP server.  I wanted to authenticate the user using a secure LDAP connection (aka LDAPS, which uses port 636.  Non-secure LDAP uses 389).

The Problem: My original implementation used the PrincipalContext class (From the first answer here: http://stackoverflow.com/questions/290548/c-sharp-validate-a-username-and-password-against-active-directory).

public bool Authenticate(string username, string password)
{
	using(var context = new PrincipalContext(ContextType.Domain, "exampledomain:636"))
	{
		// validate the credentials
		return context.ValidateCredentials(username, password);
	}
}

PrincipalContext worked perfectly fine for non-secure LDAP connections, but as soon as I tried to connect over port 636 I got a DirectoryOperationException: “The server cannot handle directory requests.”, and that’s all the information it provided.  I tried several things to fix it:

  • Verified the domain name.  To make sure there were no SSL issues, I confirmed that the domain name matched the certificate domain exactly.
  • Tried setting various options on the PrincipalContext object.
  • Telnetted to the domain server on port 636, which connected fine.
  • Wiresharked the request, then compared it to another utility (ldp.exe) that connected over LDAPS to that server successfully.  The traces looked essentially identical.

Alas, my efforts were in vain.

The Solution: Another answer on the same question pointed me in the right direction.  There’s a lower-level solution using a class called LdapConnection:

private const int LDAPError_InvalidCredentials = 0x31;
private const string Domain = "mydomain";

public bool Authenticate(string username, string password)
{
	try
	{
		using (var ldapConnection = new LdapConnection("exampledomain:636"))
		{
			var networkCredential = new NetworkCredential(username, password, Domain);
			ldapConnection.SessionOptions.SecureSocketLayer = true;
			ldapConnection.AuthType = AuthType.Negotiate;
			ldapConnection.Bind(networkCredential);
		}

		// if the bind succeeds, the credentials are OK
		return true;
	}
	catch (LdapException ldapException)
	{
		// Unfortunately, invalid credentials fall into this block with a specific error code
		if (ldapException.ErrorCode.Equals(LDAPError_InvalidCredentials)) return false;
		throw;
	}
}

The LdapConnection solution worked like a charm.  The only nuisance is that bad credentials are thrown as an exception, which I’ve accounted for in the example above.

There’s probably something stupid I missed with PrincipalContext.  The exception gave so little detail as to what was wrong, however, that it just led me to give up and try another solution.