Search code examples
c#asp.net-coreasp.net-core-identity

why password is validated twice?


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?


Solution

  • 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.