In yesterday’s post, “Leveraging LDAP Authentication in C# & .NET”, we looked at how to leverage LDAP for authentication.

The problem with LDAP, however, is that it sends the username and password in plain text over the network, making it vulnerable to man-in-the-middle attacks, as well as sniffing.

There is, however, a solution to this - LDAP over Secure Sockets Layer (SSL), better known as LDAPS.

In this protocol, the LDAP traffic is wrapped in SSL / TLS, meaning that all the usernames, passwords, queries, and responses are encrypted.

The code is very similar to the LDAP code.

using System.DirectoryServices.Protocols;
using System.Net;

namespace Authenticators;

public sealed class LDAPSAuthenticator : IAuthenticator
{
    private const int LDAPSPort = 636;
    private readonly string _domain;

    public LDAPSAuthenticator(string domain)
    {
        ArgumentException.ThrowIfNullOrEmpty(domain);
        _domain = domain;
    }

    public void Authenticate(string username, string password)
    {
        ArgumentException.ThrowIfNullOrEmpty(username);
        ArgumentException.ThrowIfNullOrEmpty(password);
        try
        {
            var identifier = new LdapDirectoryIdentifier(_domain, LDAPSPort, fullyQualifiedDnsHostName: true,
                connectionless: false);

            var connection = new LdapConnection(identifier)
            {
                Credential = new NetworkCredential(username, password),
                AuthType = AuthType.Negotiate
            };

            // Enforce SSL (LDAPS)
            connection.SessionOptions.SecureSocketLayer = true;

            // Authenticate
            connection.Bind();
        }
        catch (LdapException ex)
        {
            throw new AuthenticationException($"Ldap Exception: {ex.Message}", ex);
        }
        catch (Exception ex)
        {
            throw new AuthenticationException($"General Authentication Exception: {ex.Message}", ex);
        }
    }
}

Of interest here is the following:

  1. We are setting the AuthType of the LdapConnecton to Negotiate. The other options are here
  2. We are setting the SessionOptions to require SSL

We write some tests for the same:

using Authenticators;
using AwesomeAssertions;

namespace Tests;

[Trait("Category", "LDAPS")]
public class LDAPSAuthenticatorTests
{
    [Theory]
    [InlineData("yourDomain.com", "yourUser", "yourPassword")]
    public void Valid_Username_And_ValidPassword_Succeeds(string domain, string user, string password)
    {
        var authenticator = new LDAPSAuthenticator(domain);
        var act = () => authenticator.Authenticate(user, password);
        act.Should().NotThrow();
    }

    [Theory]
    [InlineData("yourDomain.com", "yourUser", "INVALID")]
    public void Valid_Username_And_InValidPassword_Fails(string domain, string user, string password)
    {
        var authenticator = new LDAPSAuthenticator(domain);
        var act = () => authenticator.Authenticate(user, password);
        act.Should().Throw<AuthenticationException>();
    }

    [Fact]
    public void Null_Domain_Throws_Exception()
    {
        var act = () => new LDAPSAuthenticator("");
        act.Should().Throw<ArgumentException>();
    }

    [Fact]
    public void Null_UserName_Throws_Exception()
    {
        var authenticator = new LDAPSAuthenticator("yourDomain.com");
        var act = () => authenticator.Authenticate("", "password");
        act.Should().Throw<ArgumentException>();
    }

    [Fact]
    public void Null_Password_Throws_Exception()
    {
        var authenticator = new LDAPAuthenticator("yourDomain.com");
        var act = () => authenticator.Authenticate("user", "");
        act.Should().Throw<ArgumentException>();
    }
}

TLDR

Due to the insecurity of LDAP, we can use the recommended approach of LDAPS from the System.DirectoryServices.Protocols namespace.

The code is in my GitHub.

Happy hacking!