Creating custom password validators for ASP.NET Core Identity

ASP.NET Core Identity is a membership system that lets you add user accounts to your ASP.NET Core applications. It provides the low-level services for creating users, verifying passwords and signing users in to your application, as well as additional features such as two-factor authentication (2FA) and account lockout after too many failed attempts to login.

When users register on an application, they typically provide an email/username and a password. ASP.NET Core Identity lets you provide validation rules for the password, to try and prevent users from using passwords that are too simple.

In this post, I'll talk about the default password validation settings and how to customise them. Finally, I'll show how you can write your own password validator for ASP.NET Core Identity.

The default settings

By default, if you don't customise anything, Identity configures a default set of validation rules for new passwords:

Passwords must be at least 6 characters

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

If you want to change these values, to increase the minimum length for example, you can do so when you add Identity to the DI container in ConfigureServices. In the following example I've increased the minimum password length from 6 to 10, and disabled the other validations:

Coming in ASP.NET Core 2.0

In ASP.NET Core Identity 2.0, which uses ASP.NET Core 2.0 (available as 2.0.0-preview2 at time of writing) you get another configurable default setting:

Passwords must use at least n different characters

This lets you guard against the (stupidly popular) password "111111" for example. By default, this setting is disabled for compatibility reasons (you only need 1 unique character), but you can enable it in a similar way. The following example requires passwords of length 10, with at least 6 unique characters, one upper, one lower, one digit, and one special character.

When the default validators aren't good enough..

Whether having all of these rules when creating a password is a good idea is up for debate, but it's certainly nice to have the options there. Unfortunately, sometimes these rules aren't enough to really protect users from themselves.

For example, it's quite common for a sub-set of users to use their username/email as their password. This is obviously a bad idea, but unfortunately the default password rules won't necessarily catch it! For example, in the following example I've used my username as my password:

and it meets all the rules: more than 6 characters, upper and lower, number, even a special character @!

And voilà, we're logged in...

Luckily, ASP.NET Core Identity lets you write your own password validators. Let's create a validator to catch this common no-no.

Writing a custom validator for ASP.NET Core Identity

You can create a custom validator for ASP.NET Core Identity by implementing the IPasswordValidator<TUser> interface:

One thing to note about this interface is that the TUser type parameter is only limited to class - that means that if you create the most generic implementation of this interface, you won't be able to use properties of the user parameter.

That's fine if you're validating the password by looking at the password itself, checking the length and which character types are in it etc. Unfortunately, it's no good for the validator we're trying to create - we need access to the UserName property so we can check if the password matches.

We can get round this by implementing the validator and restricting the TUser type parameter to an IdentityUser. This is the default Identity user type created by the templates (which use EF Core under the hood), so it's still pretty generic, and it means we can now build our validator.

This validator checks if the UserName of the new TUser object passed in matches the password (ignoring case). If they match, then it rejects the password using the IdentityResult.Failed method, passing in an IdentityError (and wrapping in a Task<>).

The IdentityError class has both a Code and a Description - the Code property is used by the Identity system internally to localise the errors, and the Description is obviously an English description of the error which is used by default.

Note: Your errors won't be localised by default - I'll write a follow up post about this soon.

If the password and username are different, then the validator returns IdentityResult.Success, indicating it has no problems.

Note: The default templates use the email address for both the UserName and Email properties. If your user entities are configured differently, the username is separate from the email for example, you could check the password doesn't match either property by updating the ValidateAsync method accordingly.

Now we have a validator, we just need to make Identity aware of it. You do this with the AddPasswordValidator<> method exposed on IdentityBuilder when configuring your app:

It looks a bit long-winded because we need to pass in the TUser generic parameter. If we're just building the validator for a single app, we could always remove the parameter altogether and simplify the signature somewhat:

Now when you try and use your username as a password to register a new user you'll get a nice friendly warning to tell you to stop being stupid!

Summary

The default password validation in ASP.NET Core Identity includes a variety of password rules that you configure, such as password length, and required character types.

You can write your own password validators by implementing IPasswordValidator<TUser> and calling .AddPasswordValidator<T> when configuring Identity.

I have created a small NuGet package containing the validator from this blog post, a similar validator for validating the password does not equal the email, and one that looks for specific phrases (for example the URL or domain of your website - another popular choice for security-lacking users!).