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..
Answers:
Thank you for visiting the Q&A section on Magenaut. Please note that all the answers may not help you solve the issue immediately. So please treat them as advisements. If you found the post helpful (or not), leave a comment & I’ll get back to you as soon as possible.
Method 1
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));
}
}
}
Method 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>>();
All methods was sourced from stackoverflow.com or stackexchange.com, is licensed under cc by-sa 2.5, cc by-sa 3.0 and cc by-sa 4.0