Quick and Easy ASP.NET Identity Multitenancy

Scott Brady
Scott Brady
ASP.NET Identity

Multitenancy

Multitenancy is when multiple applications share an environment. In our scenario this environment is the identity management library ASP.NET Identity v2.2 where we want to allow multiple organisations to store two different users with the same username in a single database.

Out of the box ASP.NET does not allow for multitenancy. With the ASP.NET Identity default behaviour, all organisations would share a single user in the database, they cannot create one each. This is a problem when the organisations are separate entities with no knowledge of each other and should not be sharing data. For example if a user updates their password within x.com, this will also change their password in y.com. Not good.

Some people recommend the work around of prepending usernames with an identifier for each tenant, however there is a way to extend ASP.NET identity to make it truly multitenanted.

Solution

Extending from the default Core and Entity Framework packages of ASP.NET Identity we can add a new claim for the concept of Tenant Id. With a bit of work we can use this claim to allow for duplicate usernames within a single ASP.NET Identity database.

The following package is necessary for this solution:

Install-Package Microsoft.AspNet.Identity.EntityFramework

This package will also pull in the necessary dependancies of Microsoft.AspNet.Identity.Core and EntityFramework.

IdentityUser

To start with we’ll need to add the claim of TenantId (you can rename this as fits your business requirements) by extending the IdentityUser class. Whilst this is conceptually a claim, we will take advantage of the AspNetUser table and add TenantId as a property, as we will be querying by this property a fair bit. For simplicity I have added the TenantId as an int however a non-iterative alternative would be to use a string.

public class ApplicationUser : IdentityUser {
    public int TenantId { get; set; }
}

UserStore

Next we’ll implement the UserStore for our new user that is aware of our new property. Here we are using a property within our UserStore class to set our TenantId, allowing us to override the base implementation with our multi-tenanted implementation.

public class ApplicationUserStore<TUser> : UserStore<TUser> 
  where TUser : ApplicationUser {
    public ApplicationUserStore(DbContext context)
      : base(context) {
    }

public int TenantId { get; set; } }

Now we’ll need to make some of the methods aware of the TenantId:

CreateUserAsync
public override Task CreateAsync(TUser user) {
    if (user == null) {
        throw new ArgumentNullException("user");
    }

user.TenantId = this.TenantId; return base.CreateAsync(user); }
FindByEmailAsync
public override Task<TUser> FindByEmailAsync(string email) {
    return this.GetUserAggregateAsync(u => u.Email.ToUpper() == email.ToUpper() 
        && u.TenantId == this.TenantId);
}
FindByNameAsync
public override Task<TUser> FindByNameAsync(string userName) {
    return this.GetUserAggregateAsync(u => u.UserName.ToUpper() == userName.ToUpper() 
        && u.TenantId == this.TenantId);
}

UserValidator

Whilst the default UserValidator has hardcoded checks for duplicate user names, our new implementation of the UserStore methods FindByNameAsync and FindByEmailAsync will allow for the correct multi-tenanted behaviour (assuming you have set a TenantId within the UserStore). This means we can take full advantage of the default UserValidator and extend it if necessary.

IdentityDbContext

Now here’s an awkward bit. The ASP.NET Identity team have again hardcoded a check for duplicate usernames within the IdentityDbContext class, however this time it is both within the ValidateEntity method and in the EF database schema itself using an index.

The index can be solved by extending the OnModelCreating method to change the unique index based on username to also look for our TenantId (a composite index). This saves us losing this useful index and optimises our database for multitenancy. You can do this with the following override method:

public class ApplicationUserDbContext<TUser> : IdentityDbContext<TUser> 
  where TUser : ApplicationUser {
    public ApplicationUserDbContext(string nameOrConnectionString)
      : base(nameOrConnectionString) {
    }

protected override void OnModelCreating(DbModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder);
var user = modelBuilder.Entity<TUser>();
user.Property(u => u.UserName) .IsRequired() .HasMaxLength(256) .HasColumnAnnotation("Index", new IndexAnnotation( new IndexAttribute("UserNameIndex") { IsUnique = true, Order = 1}));
user.Property(u => u.TenantId) .IsRequired() .HasColumnAnnotation("Index", new IndexAnnotation( new IndexAttribute("UserNameIndex") { IsUnique = true, Order = 2 })); } }

The ValidateEntity method is a bit more tricky however, as we will have to reimplement the entire method in order to remove the hardcoded username checks:

protected override DbEntityValidationResult ValidateEntity(
  DbEntityEntry entityEntry, IDictionary<object, object> items) {
    if (entityEntry != null && entityEntry.State == EntityState.Added) {
        var errors = new List<DbValidationError>();
        var user = entityEntry.Entity as TUser;

if (user != null) { if (this.Users.Any(u => string.Equals(u.UserName, user.UserName) && u.TenantId == user.TenantId)) { errors.Add(new DbValidationError("User", string.Format("Username {0} is already taken for AppId {1}", user.UserName, user.TenantId))); }
if (this.RequireUniqueEmail && this.Users.Any(u => string.Equals(u.Email, user.Email) && u.TenantId == user.TenantId)) { errors.Add(new DbValidationError("User", string.Format("Email Address {0} is already taken for AppId {1}", user.UserName, user.TenantId))); } } else { var role = entityEntry.Entity as IdentityRole;
if (role != null && this.Roles.Any(r => string.Equals(r.Name, role.Name))) { errors.Add(new DbValidationError("Role", string.Format("Role {0} already exists", role.Name))); } } if (errors.Any()) { return new DbEntityValidationResult(entityEntry, errors); } }
return new DbEntityValidationResult(entityEntry, new List<DbValidationError>()); }

Client

All that remains now is to initialise the classes. Don't forget you will need to supply the TenantId each time you new up the context. See the below example (note the use of 'example', these classes are all disposable...).

var context = new ApplicationUserDbContext<ApplicationUser>("DefaultConnection");
var userStore = new ApplicationUserStore<ApplicationUser>(context) { TenantId = 1 };
var userManager = new UserManager<ApplicationUser, string>(userStore);

Final Notes and Source Code

As you can see, it's a fair amount of work to multitenant ASP.NET Identity, involving hardcoded overrides and database schema modifications. It all depends on your business requirements whether or not it is worth going the distance with full multitenancy or to just start modifying strings by prepending/appending usernames.

Github

We all hate it when someone posts a guide and leaves out some of the most important steps. For this reason all of the above code will compile as a working solution, however I have also uploaded the same classes to GitHub this time including all the necessary constructors and generic classes that enable you to keep all of the extensibility features of the default ASP.NET Identity implementation.

Links

  • Blog Example Code - Full source code for the above methodology with full generic classes and constructors necessary to use the extensibility features of ASP.NET Identity