I'm configuring ASP.NET Core Identity's password validations with custom validations, so in the startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddIdentity<AppUser, IdentityRole>( opts => {
opts.Password.RequiredLength = 6;
}).AddEntityFrameworkStores<AppIdentityDbContext>().AddDefaultTokenProviders();
services.AddTransient<IPasswordValidator<AppUser>, CustomPasswordValidator>();
...
}
and my customer password validator is
public class CustomPasswordValidator : PasswordValidator<AppUser>
{
public override async Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user, string password)
{
IdentityResult result = await base.ValidateAsync(manager, user, password);
List<IdentityError> errors = result.Succeeded ? new List<IdentityError>() : result.Errors.ToList();
if (password.ToLower().Contains(user.UserName.ToLower()))
{
errors.Add(new IdentityError
{
Code = "PasswordContainsUserName",
Description = "Password cannot contain username"
});
}
return errors.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
}
}
and when I ran the app and typed an invalid password whose length < 6, there is a duplicated validation output as:
Passwords must be at least 6 characters.
Passwords must be at least 6 characters.
I guess it is because I called the base
's ValidateAsync()
(which contains the validation login in the startup.cs
), but isn't that my CustomPasswordValidator
override base
's ValidateAsync()
, so the base's validation should only be called once?
services.AddTransient<IPasswordValidator<AppUser>, CustomPasswordValidator>();
This call doesn't replace the IPasswordValidator<AppUser>
registration already added by the call to AddIdentity
; it adds another. This means you end up with two password-validators, both of which check the same set of built-in rules.
Usually, when requesting a type from DI, we ask for a single implementation. Here's an example constructor:
public SomeClass(ISomeService someService) { }
If two implementations have been registered for ISomeService
, this constructor is given an instance of the one that is registered last. However, we can still get both instances by updating the constructor to request a collection of ISomeService
. Here's an example of that:
public SomeClass(IEnumerable<ISomeService> someServices) { }
In this scenario, with two registered implementations of ISomeService
, someServices
contains instances of both implementations. This is exactly what happens in UserManager
, which was designed to support multiple validators.
Viewing the source for UserManager.ValidatePasswordAsync
shows how the validators are enumerated and executed in sequence:
foreach (var v in PasswordValidators) { var result = await v.ValidateAsync(this, user, password); if (!result.Succeeded) { errors.AddRange(result.Errors); } }
This means that, instead of extending PasswordValidator<AppUser>
, CustomPasswordValidator
can just implement IPasswordValidator<AppUser>
directly:
public class CustomPasswordValidator : IPasswordValidator<AppUser>
{
public async Task<IdentityResult> ValidateAsync(UserManager<AppUser> manager, AppUser user, string password)
{
// ...
}
}
The code inside your implementation method stays the same, except for it calling into base
.