.netldapdirectoryservicessasl

DotNet LdapConnection with authentication method external and client certificate


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:

  1. From a dotnet application using LdapConnection create a LDAP connection to Active Directory outside of my current domain
  2. Using only a username and a certificate for authentication
  3. Verifying if the user is in the correct group We are using System.DirectoryServices.Protocols 6.0.2 on .net 8.0 on Windows 11

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(...)

Solution

  • 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}");
            }
        }
    }