5

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?

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
Alex Herman
  • 2,708
  • 4
  • 32
  • 53
  • 1
    OOTB Identity has no multi tenancy. Although for an earlier version, [Quick and Easy ASP.NET Identity Multitenancy](https://www.scottbrady91.com/ASPNET-Identity/Quick-and-Easy-ASPNET-Identity-Multitenancy) covers what would need to be changed. Also see [How to use Asp.Net Core Identity in Multi-Tenant environment](https://stackoverflow.com/questions/48133171/). – Mark G Aug 01 '18 at 00:08
  • The answer above was quite useful for me. Just be sure to check if there's no user with the same Username, Email and Tennant *besides* the user being validated, otherwise any edit on said user will break. – lccarvalho Apr 12 '19 at 14:26

1 Answers1

8

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.

Camilo Terevinto
  • 31,141
  • 6
  • 88
  • 120
  • Thank you very much for your answer! Will investigate it soon. – Alex Herman Aug 01 '18 at 00:17
  • @AlexHerman You are welcome. I have added a second answer just in case :) – Camilo Terevinto Aug 01 '18 at 00:26
  • I tried the second answer, but it gives me the error "unique index 'UserNameIndex'" on registering with the same email for another tenant, how can I make UserName not unique? also, you forgot to write await before "manager.Users.AnyAsync" – rami bin tahin Nov 10 '21 at 09:51