Using ASP.NET Core Identity is great out of the box to manage users in a web app. The default configuration give pretty standard requirements for passwords when users are creating accounts.

The most simple way to change the settings can be found in the documentation

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddIdentity<ApplicationUser, IdentityRole>();
    
        services.Configure<IdentityOptions>(options =>
        {
            // Default Password settings.
            options.Password.RequireDigit = true;
            options.Password.RequireLowercase = true;
            options.Password.RequireNonAlphanumeric = true;
            options.Password.RequireUppercase = true;
            options.Password.RequiredLength = 6;
            options.Password.RequiredUniqueChars = 1;
        });
    }
}

This is great and will be enough in many cases. You could also move the settings in a seperate method to avoid cluttering the ConfigureServices method.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
         services.AddIdentity<ApplicationUser, IdentityRole>(SetIdentityOptions);
    }

    private static void SetIdentityOptions(IdentityOptions options)
    {
        options.Password.RequireDigit = true;
        options.Password.RequireLowercase = true;
        options.Password.RequireNonAlphanumeric = true;
        options.Password.RequireUppercase = true;
        options.Password.RequiredLength = 6;
        options.Password.RequiredUniqueChars = 1;
    }
}

Now if we want to make something a bit more complex, for example making sure that the password doesn’t contain the username, we’ll have to implement the IPasswordValidator interface.

public class MyPasswordValidator : IPasswordValidator<ApplicationUser>
{
    public Task<IdentityResult> ValidateAsync(UserManager<ApplicationUser> manager, ApplicationUser user, string password)
    {
        return Task.FromResult(Validate(user, password));
    }

    private IdentityResult Validate(ApplicationUser user, string password)
    {
        if (password.Contains(user.UserName, System.StringComparison.OrdinalIgnoreCase))
        {
            return IdentityResult.Failed(new IdentityError
            {
                Code = "UserNameInPassword",
                Description = "Passwords cannot contain the account's username"
            });
        }

        return IdentityResult.Success;
    }
}

We also need to set it up in the Startup.cs to have it called on password validation.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
         services
            .AddIdentity<ApplicationUser, IdentityRole>()
            .AddPasswordValidator<MyPasswordValidator>();
    }
}

This works and is easy enough to put together but what if we want to have all of our password validation in one place to make maintenance easier? For example, let’s add the length validation to our MyPasswordValidator class.

public class MyPasswordValidator : IPasswordValidator<ApplicationUser>
{
    public Task<IdentityResult> ValidateAsync(UserManager<ApplicationUser> manager, ApplicationUser user, string password)
    {
        return Task.FromResult(Validate(user, password));
    }

    private IdentityResult Validate(ApplicationUser user, string password)
    {
        if (password.Length < 6)
        {
            return IdentityResult.Failed(new IdentityError
            {
                Code = "PasswordTooShort",
                Description = "Passwords must be at least six characters"
            });
        }

        if (password.Contains(user.UserName, System.StringComparison.OrdinalIgnoreCase))
        {
            return IdentityResult.Failed(new IdentityError
            {
                Code = "UserNameInPassword",
                Description = "Passwords cannot contain the account's username"
            });
        }

        return IdentityResult.Success;
    }
}

Now if you run this code and send the password pass123, you will get a validation error saying that you must provide a password with at least 8 characters. This is because the default validator still runs when you use the AddPasswordValidator to set up your custom validator.

Let’s find out why.

When we call AddIdentity in our Startup class, it is actually an extension method calling a bunch of other methods (as you can see here https://github.com/aspnet/AspNetCore/blob/master/src/Identity/src/Identity/IdentityServiceCollectionExtensions.cs#L38). But the one that is of interest to us is services.TryAddScoped<IPasswordValidator<TUser>, PasswordValidator<TUser>>(); This method is trying to register the default PasswordValidator into the services if it isn’t already present.

Knowing that, it’s easy to skip the default validation. Registering the IPasswordValidator before the AddIdentity method does it will do the trick.

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.TryAddScoped<IPasswordValidator<ApplicationUser>, MyPasswordValidator>();
        
        // This line must come after
        services.AddIdentity<ApplicationUser, IdentityRole>();
    }
}

We have basically replaced AddPasswordValidator so we do not need it anymore. We actually could not use it for this purpose since it’s an extension method for the IdentityBuilder type.