Search code examples
c#asp.net-coremulti-tenantasp.net-core-identity

Unique UserName & Email per tenant


I'm coding a multi-tenant app using ASP.NET Core 2.1.

I would like to override the default user-creation-related validation mechanism.

Currently I cannot create several users with the same UserName.

My ApplicationUser model has a field called TenantID.

What I'm trying to achieve: UserName & EmailAddress must be unique per tenant.

I've been googling a solution, but haven't found much info for asp.net core on this one.

Most of the results would only cover Entity Framework aspects, as if it's just a matter of overriding OnModelCreating(...) method. Some are related to NON-core edition of ASP.NET Identity.

I wonder if I should keep investigating OnModelCreating approach?

Or perhaps, there's something else that needs to be overridden around Identity?


Solution

  • First of all, you need to disable the Identity's built-in validation mechanism:

    services.AddIdentity<ApplicationUser, IdentityRole>(options =>
    {
        // disable the built-in validation
        options.User.RequireUniqueEmail = false;
    })
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    

    Then, assuming you are registering users with the ASP.NET Core with Identity template, you could just do:

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null)
    {
        ViewData["ReturnUrl"] = returnUrl;
    
        if (ModelState.IsValid)
        {
            return View(model); 
        }
    
        // check for duplicates
        bool combinationExists = await _context.Users
            .AnyAsync(x => x.UserName == model.UserName 
                     && x.Email == model.Email
                     && x.TenantId == model.TenantId);
    
        if (combinationExists)
        {
            return View(model);
        }
    
        // create the user otherwise
    }
    

    If you don't want to do that kind of checking in the controller and would rather keep the Identity flow, you can create your own IUserValidator<ApplicationUser> pretty simply:

    public class MultiTenantValidator : IUserValidator<ApplicationUser>
    {
        public async Task<IdentityResult> ValidateAsync(UserManager<ApplicationUser> manager, ApplicationUser user)
        {
            bool combinationExists = await manager.Users
                .AnyAsync(x => x.UserName == user.UserName 
                            && x.Email == user.Email
                            && x.TenantId == user.TenantId);
    
            if (combinationExists)
            {
                return IdentityResult.Failed(new IdentityResult { Description = "The specified username and email are already registered in the given tentant" });
            }
    
            // here the default validator validates the username for valid characters,
            // let's just say all is good for now
            return IdentityResult.Success;
        }
    }
    

    And you would then tell Identity to use your validator:

    services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddUserValidator<MultiTenantValidator>()
        .AddEntityFrameworkStores<ApplicationDbContext>()
        .AddDefaultTokenProviders();
    

    With this, when you call UserManager.CreateAsync, the validation will take place before creating the user.