ASP.NET Core provides a lot of identity feature out of the box when individual user accounts is selected during project creation. Using the default settings a user’s password is required to be at least 6 characters and contain a number, a lower case letter, an uppercase letter and a special character. This post is going to cover changing the the above options as well as creating custom validators.
Password Options
The following is the default registration of identity in the ConfigureServices function of the Startup class with the default settings mentioned above.
services.AddIdentity<ApplicationUser, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();
AddIdentity can accept options part of which allows control over the basic characteristics of what is required for user passwords. Here is the same AddIdentity but with all the options for passwords listed.
services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonLetterOrDigit = true; options.Password.RequireUppercase = true; options.Password.RequiredLength = 6; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders();
All the options do what you would expect. One thing to note is if you change the required length by setting options.Password.RequiredLength then the new setting will only be validated on post back to the server, which is the case of most password validation anyway, but for pre-post validation on length then the string length data annotation needs to be updated on RegisterViewModel.Password, ResetPasswordViewModel.Password, ChangePasswordViewModel.NewPassword and SetPasswordViewModel.NewPassword.
Custom Password Validators
The above is great for changing simple aspects of password validation, but we all know password rules for organizations are not always simple enough to be covered by the above. Thankfully Microsoft has provided the AddPasswordValidator extension method to the IdentityBuilder class which is what is returned by AddIdentity.
AddPasswordValidator takes a type that implements IPasswordValidator. The custom validator only has to implement the ValidateAsync defined by IPasswordValidator. The following validator checks to make sure that all the characters of the password are not the same and returns an IdentityResult based on the conditions passing. Forgive the contrived example, but I wanted to keep the class as simple as possible.
public class SameCharacterPasswordValidator<TUser>: IPasswordValidator<TUser> where TUser : class { public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user, string password) { return Task.FromResult(password.Distinct().Count() == 1 ? IdentityResult.Failed(new IdentityError { Code = "SameChar", Description = "Passwords cannot be all the same character." }) : IdentityResult.Success); } }
If validation failed is the result then is added to the list of validation messages the user sees just like with the built in password validations.
Here is registration of identity with the custom password validation which is on the last line.
services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireNonLetterOrDigit = true; options.Password.RequireUppercase = true; options.Password.RequiredLength = 6; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders() .AddPasswordValidator<SameCharacterPasswordValidator<ApplicationUser>>();
Potential Use
Imagine you have a requirement to make sure a user doesn’t reuse the same password for a period of time. This would be a great place for a custom password validator. You could use dependency injection to get reference to a history of password hashes and use that to verify the user is not repeating the same password. Of course would have to first write the password hash history.
Great post!
Just one question, is this custom validate only works on the server side or works for client and sever side ?
Thank you Alex! Custom validation is done server side.
Thanks for the reply.
Is there any quick way to integrate it with Unobtrusive JavaScript? same way as data annotation works!
I don’t know of a way. The only client side validation for the standard options is length and that comes from the data annotation on the model not the from the actual password validation.
If you come across any way to get it working please let me know.
Thanks for sharing !!!
Seems like:
options.Password.RequireNonLetterOrDigit = true;
is changed to:
options.Password.RequireNonAlphanumeric = false;
Thanks for the update!
Very helpful article, thanks. Can we use multiple validators? Say I have a class that blocks classic dumb passwords, and a class that enforces no password re-use, do I just chain them as AddPasswordValidator() calls?
Thank you for the feedback Steve! Chaining AddPasswordValidators should work fine and would be what I would try first. If that failed then making a class to aggregate all of your password validation classes would be a workable fallback option.
Chaining seems to work, and the error messages are additive, which is good. I would suggest anyone trying this does their own tests first though, do not take my word for it, I have not been very thorough (yet, more later hopefully).
That is helpful information thank you for the update, Steve!
Thank yo s much for the post! It helped me greatly!
I got Below error
System.AggregateException: ‘ImplementationType: Microsoft.AspNetCore.Identity(Error while validating the service descriptor ‘ServiceType: Microsoft.AspNetCore.Identity.UserManager
1[Core.Entity.Identity.AppUser] Lifetime: Scoped ImplementationType: Microsoft.AspNetCore.Identity.UserManager
1[Core.Entity.Identity.AppUser]’: Implementation type ‘API.Helper.PasswordValidatorConditionCheck’ can’t be converted to service type ‘Microsoft.AspNetCore.Identity.IPasswordValidator1[Core.Entity.Identity.AppUser]') (Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Identity.IPasswordValidator
1[Core.Entity.Identity.AppUser] Lifetime: Scoped ImplementationType: API.Helper.PasswordValidatorConditionCheck’: Implementation type ‘API.Helper.PasswordValidatorConditionCheck’ can’t be converted to service type ‘Microsoft.AspNetCore.Identity.IPasswordValidator1[Core.Entity.Identity.AppUser]') (Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Identity.ISecurityStampValidator Lifetime: Scoped ImplementationType: Microsoft.AspNetCore.Identity.SecurityStampValidator
1[Core.Entity.Identity.AppUser]’: Implementation type ‘API.Helper.PasswordValidatorConditionCheck’ can’t be converted to service type ‘Microsoft.AspNetCore.Identity.IPasswordValidator1[Core.Entity.Identity.AppUser]') (Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Identity.ITwoFactorSecurityStampValidator Lifetime: Scoped ImplementationType: Microsoft.AspNetCore.Identity.TwoFactorSecurityStampValidator
1[Core.Entity.Identity.AppUser]’: Implementation type ‘API.Helper.PasswordValidatorConditionCheck’ can’t be converted to service type ‘Microsoft.AspNetCore.Identity.IPasswordValidator1[Core.Entity.Identity.AppUser]') (Error while validating the service descriptor 'ServiceType: Microsoft.AspNetCore.Identity.SignInManager
1[Core.Entity.Identity.AppUser] Lifetime: Scoped ImplementationType: Microsoft.AspNetCore.Identity.SignInManager1[Core.Entity.Identity.AppUser]': Implementation type 'API.Helper.PasswordValidatorConditionCheck' can't be converted to service type 'Microsoft.AspNetCore.Identity.IPasswordValidator
1[Core.Entity.Identity.AppUser]’) (Error while validating the service descriptor ‘ServiceType: Microsoft.AspNetCore.Identity.UserManager1[Core.Entity.Identity.AppUser] Lifetime: Scoped ImplementationType: Microsoft.AspNetCore.Identity.UserManager
1[Core.Entity.Identity.AppUser]’: Implementation type ‘API.Helper.PasswordValidatorConditionCheck’ can’t be converted to service type ‘Microsoft.AspNetCore.Identity.IPasswordValidator`1[Core.Entity.Identity.AppUser]’)’