Authenticate ASP.NET Core Identity Users via Active Directory or LDAP Password

Update: I have published an updated 2.0.0-preview00 release that supports ASP.NET Core Identity 2.0 on .NET Standard 2.0 at NuGet.org. I'll publish 2.0.0 without the "preview" tag once I hear back from a couple folks that this resolved their reported issues.

In a project I was recently working on, I needed a way to store and manage user accounts in a stock ASP.NET Core Identity Entity Framework Core based database, but validate user passwords against an existing Active Directory domain. In this situation, I could not leverage Kerberos/Windows Authentication because users were outside the Intranet, nor could I use ADFS or equivalent SSO services as it was beyond the scope of my project to deploy such a solution.

To achieve this, I created a simple UserManager wrapper class that overrides the base CheckPasswordAsync method with one that uses the Novell LDAP library for NETStandard 1.3 to perform an LDAP bind against a directory, and thus perform simple password validation.

I began by creating a UserManager class that inherits from Microsoft.AspNetCore.Identity.UserManager.

/// <summary>
/// Provides a custom user store that overrides password related methods to valid the user's password against LDAP.
/// </summary>
/// <typeparam name="TUser"></typeparam>
public class LdapUserManager<TUser> : Microsoft.AspNetCore.Identity.UserManager<TUser>
where TUser: class

Then I implement CheckPasswordAsync() using an LdapAuthentication class, which is just a loose abstraction around the Novell LDAP library.

/// <summary>
/// Checks the given password agains the configured LDAP server.
/// </summary>
/// <param name="user"></param>
/// <param name="password"></param>
/// <returns></returns>
public override async Task<bool> CheckPasswordAsync(TUser user, string password)
{
    using (var auth = new LdapAuthentication(_ldapOptions))
    {
        string dn;

        // This gives a custom way to extract the DN from the user if it is different from the username.
        if (this.Store is IUserLdapStore<TUser>)
        {
            dn = await((IUserLdapStore<TUser>)this.Store).GetDistinguishedNameAsync(user);
        }
        else
        {
            dn = await this.Store.GetNormalizedUserNameAsync(user, CancellationToken.None);
        }

        if (auth.ValidatePassword(dn, password))
        {
            return true;
        }
    }

    return false;
}

The meat of the LdapAuthentication class is in the ValidatePassword() method.

/// <summary>
/// Gets a value that indicates if the password for the user identified by the given DN is valid.
/// </summary>
/// <param name="distinguishedName"></param>
/// <param name="password"></param>
/// <returns></returns>
public bool ValidatePassword(string distinguishedName, string password)
{
    if (_isDisposed)
    {
        throw new ObjectDisposedException(nameof(LdapConnection));
    }

    if (string.IsNullOrEmpty(_options.Hostname))
    {
        throw new InvalidOperationException("The LDAP Hostname cannot be empty or null.");
    }

    _connection.Connect(_options.Hostname, _options.Port);

    try
    {
        _connection.Bind(distinguishedName, password);
        return true;
    }
    catch (Exception ex)
    {
        System.Diagnostics.Debug.WriteLine(ex.Message);
        return false;
    }
    finally
    {
        _connection.Disconnect();
    }
}

At this point, I just needed some basic configuration and DI code to get things wired up in the Startup.cs of an ASP.NET Core app.

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<Justin.AspNetCore.LdapAuthentication.LdapAuthenticationOptions>(this.Configuration.GetSection("ldap"));
    services.AddLdapAuthentication<ApplicationUser>();
    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddUserManager<Justin.AspNetCore.LdapAuthentication.LdapUserManager<ApplicationUser>>()
        .AddEntityFrameworkStores<ApplicationDbContext>()                
        .AddDefaultTokenProviders();
}

This expects configuration to come from an AppSettings.json section, which looks like this:

"ldap": {
  "Hostname": "dc1.example.com",
  "Port": 389
}

This allows me to keep the user accounts in a database (in this instance, a MySQL database), but eliminates the need for the user to have a separate password. It’s important to note that in my case, users do not need to be able to change, reset, or otherwise manage their user account password through the web interface, as they have a separate existing process in place for that.

I intend on coming back at some point an implementing more of the UserManager methods that *can* be implemented via LDAP, but for now all I needed was to eliminate the need for users to create a separate account password for this app.

The full source code is available on GitHub, or  you can install the NuGet package:

Install-Package -Pre Justin.AspNetCore.LdapAuthentication

ASP.NET MVC HtmlHelper Extension Method for Menu Highlighting

I built an extension method on the ASP.NET MVC HtmlHelper class to handle the creation of navigation menu links (tabs) that have a different CSS class applied to them if they are active (i.e. the current page). I had been doing this manually in the master layout (_Layout.cshtml) view with a bunch of if blocks. Of course you can roll out a full-fledged sitemap to handle this, but I didn’t want to.

/// <summary>
/// Extension method for <see cref="HtmlHelper"/> to support highlighting the active tab on the default MVC menu
/// </summary>
/// <param name="htmlHelper"></param>
/// <param name="linkText">The text to display in the link</param>
/// <param name="actionName">Link target action name</param>
/// <param name="controllerName">Link target controller name</param>
/// <param name="activeClass">The CSS class to apply to the link if active</param>
/// <param name="checkAction">If true, checks the current action name to determine if the menu item is 'active', otherwise only the controller name is matched</param>
/// <returns></returns>
public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string activeClass, bool checkAction)
{
    string currentAction = htmlHelper.ViewContext.RouteData.GetRequiredString("action");
    string currentController = htmlHelper.ViewContext.RouteData.GetRequiredString("controller");

    if (string.Compare(controllerName, currentController, StringComparison.OrdinalIgnoreCase) == 0 && ((!checkAction) || string.Compare(actionName, currentAction, StringComparison.OrdinalIgnoreCase) == 0))
    {
        return htmlHelper.ActionLink(linkText, actionName, controllerName, null, new { @class = activeClass });
    }

    return htmlHelper.ActionLink(linkText, actionName, controllerName);
    
}

activeClass sets the CSS class name that will be applied, and setting checkAction to true applies the class only if the action name and controller name match.

So my in my _Layout.cshtml view, I changed all the Html.ActionLink calls to Html.MenuLink and added the active and checkAction parameters.

<nav>
    <ul id="menu">                        
        <li>@Html.MenuLink("Home", "Index", "Home", "active", true)</li>
        <li>@Html.MenuLink("About", "About", "Home", "active", true)</li>
        <li>@Html.MenuLink("Contact", "Contact", "Home", "active", true)</li>
        <li>@Html.MenuLink("Something Else", "Index", "Something", "active", false)</li>
    </ul>
</nav>   

The Home, About and Contact actions are all handled by the HomeController, and since they are distinct menu choices, we set checkAction to true. The Something Else option, on the other hand, should be highlighted anytime we hit an action on the SomethingController.

One final tweak was to add an overload to default the checkAction parameter to true.

public static MvcHtmlString MenuLink(this HtmlHelper htmlHelper, string linkText, string actionName, string controllerName, string activeClass)
{
    return MenuLink(htmlHelper, linkText, actionName, controllerName, activeClass, true);
}

And the updated view code:

<nav>
    <ul id="menu">                        
        <li>@Html.MenuLink("Home", "Index", "Home", "active")</li>
        <li>@Html.MenuLink("About", "About", "Home", "active")</li>
        <li>@Html.MenuLink("Contact", "Contact", "Home", "active")</li>
        <li>@Html.MenuLink("Something Else", "Index", "Something", "active", false)</li>
    </ul>
</nav>