blog post image
Andrew Lock avatar

Andrew Lock

~6 min read

Creating a validator to check for common passwords in ASP.NET Core Identity

In my last post, I showed how you can create a custom validator for ASP.NET Core. In this post, I introduce a package that lets you validate that a password is not one of the most common passwords users choose.

You can find the package on GitHub and on NuGet, and can install it using dotnet add package CommonPasswordValidator. Currently, it supports ASP.NET Core 2.0 preview 2.

Full disclosure, this post is 100% inspired by the codinghorror.com article by Jeff Atwood on how they validate passwords in Discourse. If you haven't read it yet, do it now!

As Jeff describes in the appropriately named article Password Rules Are Bullshit, password rules can be a real pain. Obviously in theory, password rules make sense, but reality can be a bit different. The default Identity templates require:

  • Passwords must have at least one lowercase ('a'-'z')
  • Passwords must have at least one uppercase ('A'-'Z')
  • Passwords must have at least one digit ('0'-'9')
  • Passwords must have at least one non alphanumeric character

All these rules will theoretically increase the entropy of any passwords a user enters. But you just know that's not really what happens.

All it means is that instead of entering password, they enter Password1!

And on top of that, if you're using a password manager, these password rules can get in the way. So your 40 character random password happens to not have a digit in this time? Pretty sure it's still OK... should you really have to generate a new password?

Instead, Jeff Attwood suggests 5 pieces of advice when designing your password validation:

  1. Password rules are bullshit - These rarely achieve their goal, don't make the passwords of average users better, and penalise users using password managers.

You can easily disable password rules in ASP.NET Core Identity by disabling the composition rules.

  1. Enforce a minimum Unicode password length - Length is an easy rule for users to grasp, and in general, a longer password will be more secure than a short one

You can similarly set the minimum length in ASP.NET Core Identity using the options pattern, e.g. options.Password.RequiredLength = 10

  1. Check for common passwords - There's plenty of stats on the terrible password choices user make to their own devices, and you an create your own by checking out password lists available online. For example, 30% have a password from the top 10,000 most common passwords!

In this post I'll describe a custom validator you can add to your ASP.NET Core Identity project to prevent users using the most common passwords

  1. Check for basic entropy - Even with a length requirement, and checking for common passwords, users can make terrible password choices like 9999999999. A simple approach to tackling this is to require a minimum number of unique digits.

In ASP.NET Core Identity 2.0, you can require a minimum number of required digits using options.Password.RequiredUniqueChars = 6

  1. Check for special case passwords - User's shouldn't be allowed to use their username, email or other obvious values as their password.

You can create custom validators for ASP.NET Core Identity, as I showed in my previous post.

Whether you agree 100% with these rules doesn't really matter, but I think most people will agree with at least a majority of them. Either way, preventing the most common passwords is somewhat of a no-brainer.

There's no built-in way of achieving this, but thanks to ASP.NET Core Identity's extensibility, we can create a custom validator instead.

Creating a validator to check for common passwords

ASP.NET Core Identity lets you register custom password validators. These are executed when a user registers on your site, or changes their password, and let you apply additional constraints to the password.

In my last post, I showed how to create custom validators. Creating a validator to check for common passwords is pretty simple - we load the list of forbidden passwords into a HashSet, and check that the user's password is not one of them:

public class Top100PasswordValidator<TUser> : IPasswordValidator<TUser>
        where TUser : class
{
    static readonly HashSet<string> Passwords { get; } = PasswordLists.Top100Passwords;

    public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager,
                                                TUser user,
                                                string password)
    {
        if(Passwords.Contains(password))
        {
            var result = IdentityResult.Failed(new IdentityError
            {
                Code = "CommonPassword",
                Description = "The password you chose is too common."
            });
            return Task.FromResult(result);
        }
        return Task.FromResult(IdentityResult.Success);
    }
}

This validator is pretty standard. We have a list of passwords that you are not allowed to use, stored in the static HashSet<string>. ASP.NET Core Identity will call ValidateAsync when a new user registers, passing in the new user object, and the new password.

As we don't need to access the user object itself, we can make this validator completely generic to TUser, instead of limiting it to IdentityUser<TKey> as we did in my last post.

There's plenty of different passwords list we could choose from, so I chose to implement a few different variations, based on the 10 million passwords from 2016, depending on how restrictive you want to be.

  • Block passwords in the top 100 most common
  • Block passwords in the top 500 most common
  • Block passwords in the top 1,000 most common
  • Block passwords in the top 10,000 most common
  • Block passwords in the top 100,000 most common

Each of these passwords lists is stored as an embedded resource in the NuGet package. In the new .csproj file format, you do this by removing it from the normal wildcard inclusion, and marking as EmbeddedResource:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <None Remove="PasswordLists\10_million_password_list_top_100.txt" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Include="PasswordLists\10_million_password_list_top_100.txt" />
  </ItemGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.0.0-preview2-final" />
  </ItemGroup>

</Project>

With the lists embedded in the dll, we can simply load the passwords from the embedded resource into a HashSet.

Loading a list of strings from an embedded resource

You can read an embedded resource as a stream from the assembly using the GetManifestResourceStream() method on the Assembly type. I created a small helper class that loads the embedded file from the assembly, reads it line by line, and adds the password to the HashSet (using a case-insensitive string comparer).

internal static class PasswordLists
{
    private static HashSet<string> LoadPasswordList(string resourceName)
    {
        HashSet<string> hashset;

        var assembly = typeof(PasswordLists).GetTypeInfo().Assembly;
        using (var stream = assembly.GetManifestResourceStream(resourceName))
        {
            using (var streamReader = new StreamReader(stream))
            {
                hashset = new HashSet<string>(
                    GetLines(streamReader),
                    StringComparer.OrdinalIgnoreCase);
            }
        }
        return hashset;
    }

    private static IEnumerable<string> GetLines(StreamReader reader)
    {
        while (!reader.EndOfStream)
        {
            yield return reader.ReadLine();
        }
    }
}

NOTE: When you pass in the resourceName to load, it must be properly namespaced. The namespace is based on the namespace of the Assembly, and the subfolder of the resource file.

Adding the custom validator to ASP.NET Core Identity

That's all there is to the validator itself. You can add it to the ASP.NET Core Identity validators collection using the AddPasswordValidator<>() method. For example:

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddPasswordValidator<Top100PasswordValidator<ApplicationUser>>();

It's somewhat of a convention to create helper extension methods in ASP.NET Core, so we can easily add an additional extension that simplifies the above slightly:

public static class IdentityBuilderExtensions
{        
    public static IdentityBuilder AddTop100PasswordValidator<TUser>(this IdentityBuilder builder) where TUser : class
    {
        return builder.AddPasswordValidator<Top100PasswordValidator<TUser>>();
    }
}

With this extension, you can add the validator using the following:

services.AddIdentity<ApplicationUser, IdentityRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    .AddTop100PasswordValidator<ApplicationUser>();

With the validator in place, if a user tries to use a password that's too common, they'll get a standard warning when registering on your site:

Password is too common

Summary

This post was based on the suggestion by Jeff Attwood that we should limit password composition rules, focus on length, and ensure users can't choose common passwords.

ASP.NET Core Identity lets you add custom validators. This post showed how you could create a validator that ensures the entered password isn't in the top 100 - 100,000 of the 10 million most common passwords.

You can view the source code for the validator on GitHub, or you can install the NuGet package using the command

dotnet add package CommonPasswordValidator

Currently, the package targets .NET Core 2.0 preview 2. If you have any comments, suggestions, or bugs, please raise an issue or leave a comment! Thanks

Andrew Lock | .Net Escapades
Want an email when
there's new posts?