Summary of this question: does someone know how to use a client certificate to authenticate over LDAP to Active Directory without using a password from a dotnet application? Or at least can someone shed some light on how this is supposed to work?
What we are trying to accomplish:
The documentation seems to contradict each other, because this document (https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/8e73932f-70cf-46d6-88b1-8d9f86235e81) seems to suggest a bind on an client certificate authenticated TLS connection is not allowed, in contrast the RFC seems to suggest its mandatory to send a bind: https://datatracker.ietf.org/doc/html/rfc2830#ref-AuthMeth.
We hit a bug in the library initially which was picked up, but I'm not sure if it was the only one (see https://github.com/dotnet/runtime/issues/113154#issuecomment-2705206987).
Our attempts: Connecting with TLS on port 636 results in Bind failing on "Authentication method not supported".
string ldapPath = "dc";
int LDAPPort = 636;
var username = "user";
using LdapConnection ldapConnection = new LdapConnection(new LdapDirectoryIdentifier(ldapPath, LDAPPort));
ldapConnection.AuthType = AuthType.External;
LdapSessionOptions options = ldapConnection.SessionOptions;
options.SecureSocketLayer = true;
options.ProtocolVersion = 3;
ldapConnection.ClientCertificates.Add(cert);
ldapConnection.Bind();
// Perform further actions after this
Connecting without TLS on port 389 and starting TLS afterwards results in Bind failing on "Authentication method not supported".
string ldapPath = "dc";
int LDAPPort = 389;
var username = "user";
using LdapConnection ldapConnection = new LdapConnection(new LdapDirectoryIdentifier(ldapPath, LDAPPort));
ldapConnection.AuthType = AuthType.External;
ldapConnection.SessionOptions.ProtocolVersion = 3;
ldapConnection.ClientCertificates.Add(cert);
ldapConnection.SessionOptions.StartTransportLayerSecurity(null);
ldapConnection.Bind();
// Perform further actions after this
Connecting with TLS on port 636, skipping the bind and turning autobind off results in search failing with "In order to perform this operation a successful bind must be completed on the connection".
string ldapPath = "dc";
int LDAPPort = 636;
var username = "user";
using LdapConnection ldapConnection = new LdapConnection(new LdapDirectoryIdentifier(ldapPath, LDAPPort));
ldapConnection.AuthType = AuthType.External;
ldapConnection.SessionOptions.SecureSocketLayer = true;
ldapConnection.SessionOptions.ProtocolVersion = 3;
ldapConnection.AutoBind = false;
ldapConnection.ClientCertificates.Add(cert);
ldapConnection.SendRequest(...)
For anyone stumbling across this same issue: the option "ReferralChasing" needed to be set to "None" after which the beneath code worked.
using System.DirectoryServices.Protocols;
using System.Net;
using System.Security.Cryptography.X509Certificates;
namespace AuthTest;
public class SystemLdap
{
public static void Run(X509Certificate2 smartCardCert)
{
Console.WriteLine($"Running SystemLdap...{smartCardCert.Subject}");
string ldapPath = "dc";
int LDAPPort = 636;
using LdapConnection ldapConnection = new LdapConnection(new LdapDirectoryIdentifier(ldapPath, LDAPPort));
ldapConnection.AuthType = AuthType.External;
ldapConnection.SessionOptions.ReferralChasing = ReferralChasingOptions.None;
ldapConnection.SessionOptions.SecureSocketLayer = true;
ldapConnection.SessionOptions.ProtocolVersion = 3;
ldapConnection.ClientCertificates.Add(smartCardCert);
try
{
var username = "user";
// Perform a search
var searchRequest = new SearchRequest(
$"DC=dc,DC=nl",
$"(&(objectClass=user)(|(cn={username})(sAMAccountName={username})))",
SearchScope.Subtree
);
var response = (SearchResponse)ldapConnection.SendRequest(searchRequest);
foreach (SearchResultEntry entry in response.Entries)
{
Console.WriteLine($"User found: {entry.DistinguishedName}");
}
}
catch (Exception ex)
{
Console.WriteLine($"LDAP Authentication failed: {ex}");
}
}
}