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

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

Secure Remote Access through Application Specific and One-time Passwords

This is the first part of a series I will be doing on how I implemented one-time password support for remote access to resources inside my home network such as my webmail client, along with supporting application and device specific passwords for use on my mobile phone, tablet, etc.

I always hate the feeling of using any of my username and password combinations on sketchy public computer somewhere. You know the kind I am talking about, those computers at hotels running Windows XP and IE6, signed as "Adminstrator", with every toolbar and add-on installed from a 9 year old version of Real Player to three different versions of some Internet poker game. There's bound to be a key logger in there someplace.

One time use passwords have been around for a long time to mitigate this type of scenario. As the name suggests a one-time password, is valid only once. In theory if someone sees or captures that password, it's worthless to them. Typically one time passwords are accompanied by a normal password, or a PIN number. This mostly satisfies Two Factor Authentication, which requires something you know (the password or PIN), and something you have (the phone giving you the password). Google started offering one-time-password support for Google accounts through the 2-step verification system and the Authenticator app.

So we have three different surfaces to protect:

  • Apache HTTP access to the Roundcube webmail client.
  • Postfix SMTP access to send mail from an external e-mail.
  • Dovecot IMAP access to retrieve mail from an external e-mail client.

We want to be able to use our one-time password to access #1, but since an e-mail client may login many times during a single session, #2 and #3 are better served by an application specific password that is sufficiently random but never changes.

So I set out by sketching out a few requirements:

  1. Make use of my existing e-mail platform (Ubuntu 12.04 + Postfix + Dovecot + RoundCube on Apache).
  2. Close any existing access points into home network via simple username/password combinations.
  3. Support application/device specific passwords for IMAP and SMTP clients (thunderbird, etc.).
  4. Support one-time passwords to access RoundCube webmail.
  5. Phone app to generate one-time passwords.
  6. Backup codes that can be printed to use in the case where the app is not inaccessible.

Application Specific Passwords

Since the first step is to prevent logins to IMAP and SMTP from outside the firewall with passwords we may be typing in on public computer, we need to provide secure passwords that will only be entered once into a device or application to configure. We don’t need to remember these passwords, so we can revoke them and re-configure an application or device at anytime.

Dovecot is configured to authenticate users against PAM, and PAM is configured to authenticate users with mod-auth-kerb. Postfix is configured to authenticate via SASL to Dovecot. So ultimately, there is a single username/password for all users through my Kerberos database.

Dovecot separates the concept of a user database and password database, so I can keep my existing user database (Linux passwd, LDAP, etc), and just alter the password database. To add additional password validation options to Dovecot, you simply add more passdb options to the configuration file. One of those options happens to be a MySQL, so I went ahead and made a simple database and table to store our application specific passwords.

CREATE TABLE `dovecot_passwords` (
  `username` varchar(100) NOT NULL,
  `appname` varchar(50) NOT NULL,
  `password` varbinary(256) NOT NULL,
  PRIMARY KEY (`username`,`appname`)
)

You will notice username and appname make the unique key here, since we want to have multiple passwords for the same account. The data in this table might look something like this:

username appname password
justin K9 Mail on Phone ********
justin Thunderbird on Laptop ********
justin Thunderbird on Desktop ********
sarah iPhone ********

The value in the password field is the MD5 hash of the password without any whitespace (Yes, it should be salted and maybe using SHA1 instead). In order to make the application specific passwords more secure, I’m using rather long passwords, and so when I generate them I usually format them in blocks of four separated by spaces, such as xRtg Dbea 4d9g aP44. This is easier to type into a mobile keyboard while glancing back and forth between the device and the keyboard. The password database will need to ignore the whitespace, because we don’t care either way if it is there. For now I'll just manage entries in this table manually, but later I plan on writing a fancy CLI or GUI tool. So to insert new records in this table, I generate a random password, and do an insert:

INSERT INTO dovecot_passwords (username, appname, password) VALUES( 'justin', 'smartphone', MD5('xRtgDbea4d9gaP44') );

Now I can configure Dovecot to check this password database instead of PAM by changing my passdb entry to use the SQL driver instead of PAM.

#/etc/dovecot/dovecot.conf
passdb { 
    args = /etc/dovecot/dovecot-sql-other.conf 
    driver = sql 
}

And the associated SQL config file:

#/etc/dovecot/dovecot-sql-other.conf 
driver = mysql 
connect = host=localhost dbname=mail_db user=dovecot password=******** 
default_pass_scheme = PLAIN 
password_query = SELECT NULL AS password,'Y' as nopassword, username AS user 
                FROM dovecot_passwords
                WHERE username = '%u' AND password=MD5(REPLACE('%w',' ',''))

You can read more about how this configuration file works on the Dovecot Wiki page for SQL passdb, but essentially my query is removing any whitespace from the supplied password, and matching the MD5 hash. To really make this secure, we should be adding a password salt into the mix and probably using SHA1 for the hash algorithm. It is worth mentioning, that if you want to support regular username/password authentication via PAM for users on the internal network, but the application specific passwords everywhere else, this is possible by adding the pam_access module into your PAM configuration for Dovecot.

So now we have satisfied requirements #1, #2, and #3. Dovecot (and Postfix by means of SASL) will now authenticate users against the custom password database, and will not authenticate users with their old username/password.

Next up: Implementing #4, #5, #6 to support One-time passwords.