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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.