7

Helloes,

I have a .NetCore MVC APP with Identity and using this guide I was able to create custom user validators.

public class UserDomainValidator<TUser> : IUserValidator<TUser> where TUser : IdentityUser { private readonly List<string> _allowedDomains = new List<string> { "elanderson.net", "test.com" }; public Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user) { if (_allowedDomains.Any(allowed => user.Email.EndsWith(allowed, StringComparison.CurrentCultureIgnoreCase))) { return Task.FromResult(IdentityResult.Success); } return Task.FromResult( IdentityResult.Failed(new IdentityError { Code = "InvalidDomain", Description = "Domain is invalid." })); } } 

and succesfully validate my User creation by adding it to my Identity service in DI

services.AddIdentity<ApplicationUser, IdentityRole>(options => { options.User.AllowedUserNameCharacters = "abccom."; options.User.RequireUniqueEmail = true; }) .AddEntityFrameworkStores<ApplicationDbContext>() .AddDefaultTokenProviders() .AddUserValidator<UserDomainValidator<ApplicationUser>>(); 

Now, one of the existing validatiors in Identity states that the username must be unique

private async Task ValidateUserName(UserManager<TUser> manager, TUser user, ICollection<IdentityError> errors) { var userName = await manager.GetUserNameAsync(user); if (string.IsNullOrWhiteSpace(userName)) { errors.Add(Describer.InvalidUserName(userName)); } else if (!string.IsNullOrEmpty(manager.Options.User.AllowedUserNameCharacters) && userName.Any(c => !manager.Options.User.AllowedUserNameCharacters.Contains(c))) { errors.Add(Describer.InvalidUserName(userName)); } else { var owner = await manager.FindByNameAsync(userName); if (owner != null && !string.Equals(await manager.GetUserIdAsync(owner), await manager.GetUserIdAsync(user))) { errors.Add(Describer.DuplicateUserName(userName)); } } } 

Since in my app my login is done via Tenant + Username / Tenant + Email, I want to allow duplicated usernames... has anyone done something similar or have any ideas?

I need to remove this validation and I guess to adapt the SignInManager or something so it can sign in the correct user..

2 Answers 2

17

Instead of adding a new validator replace the one added in services, and create your own UserValidator.

 services.Replace(ServiceDescriptor.Scoped<IUserValidator<User>, CustomUserValidator<User>>()); public class CustomUserValidator<TUser> : IUserValidator<TUser> where TUser : class { private readonly List<string> _allowedDomains = new List<string> { "elanderson.net", "test.com" }; public CustomUserValidator(IdentityErrorDescriber errors = null) { Describer = errors ?? new IdentityErrorDescriber(); } public IdentityErrorDescriber Describer { get; } public virtual async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user) { if (manager == null) throw new ArgumentNullException(nameof(manager)); if (user == null) throw new ArgumentNullException(nameof(user)); var errors = new List<IdentityError>(); await ValidateUserName(manager, user, errors); if (manager.Options.User.RequireUniqueEmail) await ValidateEmail(manager, user, errors); return errors.Count > 0 ? IdentityResult.Failed(errors.ToArray()) : IdentityResult.Success; } private async Task ValidateUserName(UserManager<TUser> manager, TUser user, ICollection<IdentityError> errors) { var userName = await manager.GetUserNameAsync(user); if (string.IsNullOrWhiteSpace(userName)) errors.Add(Describer.InvalidUserName(userName)); else if (!string.IsNullOrEmpty(manager.Options.User.AllowedUserNameCharacters) && userName.Any(c => !manager.Options.User.AllowedUserNameCharacters.Contains(c))) { errors.Add(Describer.InvalidUserName(userName)); } } private async Task ValidateEmail(UserManager<TUser> manager, TUser user, List<IdentityError> errors) { var email = await manager.GetEmailAsync(user); if (string.IsNullOrWhiteSpace(email)) errors.Add(Describer.InvalidEmail(email)); else if (!new EmailAddressAttribute().IsValid(email)) { errors.Add(Describer.InvalidEmail(email)); } else if (_allowedDomains.Any(allowed => email.EndsWith(allowed, StringComparison.CurrentCultureIgnoreCase))) { errors.Add(new IdentityError { Code = "InvalidDomain", Description = "Domain is invalid." }); } else { var byEmailAsync = await manager.FindByEmailAsync(email); var flag = byEmailAsync != null; if (flag) { var a = await manager.GetUserIdAsync(byEmailAsync); flag = !string.Equals(a, await manager.GetUserIdAsync(user)); } if (!flag) return; errors.Add(Describer.DuplicateEmail(email)); } } } 
Sign up to request clarification or add additional context in comments.

2 Comments

did not know about services.Replace, this is actually a very good idea and very straightforward! Thanks for the solution and for your time! ;)
Another option could be to Add your custom validation first before the default validation as suggested here: jerriepelser.com/blog/… // IMPORTANT: This line must be registered before the call to AddDefaultIdentity services.TryAddScoped<IUserValidator<IdentityUser>, SpammyAddressUserValidator<IdentityUser>>();
2

Answer for those who just want to extend existing default user validation, without the risk of breaking something.

You can use the Decorator pattern and instead of copying/changing default UserValidator you can just perform additional validation of the user data. Here is an example:

public class UserValidatorDecorator<TUser> : IUserValidator<TUser> where TUser : ApplicationUser { // Default UserValidator private readonly UserValidator<TUser> _userValidator; // Some class with additional options private readonly AdditionalOptions _additionalOptions; // You can use default error describer or create your own private readonly IdentityErrorDescriber _errorDescriber; public UserValidatorDecorator(UserValidator<TUser> userValidator, AdditionalOptions additionalOptions, IdentityErrorDescriber errorDescriber) { _userValidator = userValidator; _additionalOptions = additionalOptions; _errorDescriber = errorDescriber; } public async Task<IdentityResult> ValidateAsync(UserManager<TUser> manager, TUser user) { // call to default validator var identityResult = await _userValidator.ValidateAsync(manager, user); // if default validation is already failed you can just return result, otherwise call // your additional validation method return identityResult.Succeeded ? AdditionalValidation(user) : identityResult; } public IdentityResult AdditionalUserNameValidation(TUser user) { // now you can check any value, if you need you can pass to method // UserManager as well var someValue = user.SomeValue; if (someValue < _additionalOptions.MaximumValue) { return IdentityResult.Failed(_errorDescriber.SomeError(userName)); } return IdentityResult.Success; } } 

And then you need to register your decorator, it depends on version of .NET framework, I use such code for .NET Core 3.0:

// First register default UserValidator and your options class services.AddScoped<UserValidator<ApplicationUser>>(); services.AddScoped<AdditionalOptions>(); // Then register Asp Identity and your decorator class by using AddUserValidator method services.AddIdentity<UserData, IdentityRole>() .AddEntityFrameworkStores<ApplicationDbContext>() .AddUserValidator<UserValidatorDecorator<UserData>>(); 

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.