Simple approach to multi-tenancy with Identity Framework. How to?

2.9k views Asked by At

I'd like to support multi-tenancy in my web api app that uses Identity Framework for authentication. I plan to have a single site and a single database with multiple tenants having multiple users. I'd be happy with prefixing user names with tenant id for login purposes and then strip it when returning it to the client app.
However, I'm not sure how to 'intercept' the user login. That is, where would I implement prefixing user names with tenant ids before they are passed to the framework for authentication.
Can someone please let me know how to deal with the above. A small example and pointers to on-line docs would be appreaciated.

Thanks.

1

There are 1 answers

3
Ali Zeinali On

Here there is a solution by Scott Brady, you can read explanation there i just write code here with a bit of updates.

Solution:

Add a new property to your user to determine tenants.

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

Then you need to customize your UserStore to make it aware of your new user property.

 public class ApplicationUserStore<TUser> : UserStore<TUser> 
   where TUser : ApplicationUser {
      public ApplicationUserStore(DbContext context, int tenantId)
        : base(context) {
              TenantId = tenantId
      }

      public int TenantId { get; set; }


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

          user.TenantId = this.TenantId;
          return base.CreateAsync(user);
      }

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

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


       public override Task<IdnUser> FindByIdAsync(long userId)
       {
           return this.GetUserAggregateAsync(u => u.Id == userId && u.TenantId == this.tenantId);
       }
}

and also you need to customize IdentityDbContext to support multitenancy.

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 }));
    }


   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>());
    }
}

And then you can create seprate user manager for each tenant like this:

User1:

public class AppCustomerManager : UserManager<ApplicationUser>
    {
        public AppCustomerManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
            UserTokenProvider = new PhoneNumberTokenProvider<ApplicationUser >();
        }

        public static AppCustomerManager Create(IdentityFactoryOptions<AppCustomerManager> options, IOwinContext context)
        {
            var appDbContext = context.Get<ApplicationUserDbContext>();

            //here you define your tenant by passing an int to userstore
            var appUserManager = new AppCustomerManager(new ApplicationUserStore(appDbContext, 1));

            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                appUserManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"))
                {
                    TokenLifespan = TimeSpan.FromHours(6)
                };

            }


            return appUserManager;
        }
    }

User2:

public class AppDeliveryManager : UserManager<ApplicationUser>
    {
        public AppDeliveryManager(IUserStore<ApplicationUser> store)
            : base(store)
        {
            UserTokenProvider = new PhoneNumberTokenProvider<ApplicationUser >();
        }

        public static AppDeliveryManager Create(IdentityFactoryOptions<AppDeliveryManager> options, IOwinContext context)
        {
            var appDbContext = context.Get<IdnDbContext>();
            //here you define your tenant by passing an int to userstore
            var appUserManager = new AppDeliveryManager(new ApplicationUserStore(appDbContext, 2));

            var dataProtectionProvider = options.DataProtectionProvider;
            if (dataProtectionProvider != null)
            {
                appUserManager.UserTokenProvider = new DataProtectorTokenProvider<ApplicationUser>(dataProtectionProvider.Create("ASP.NET Identity"))
                {
                    TokenLifespan = TimeSpan.FromHours(6)
                };

            }

            return appUserManager;
        }
    }

Update:

 var appUserManager = new AppDeliveryManager(new ApplicationUserStore(appDbContext, tenantId));

and tenantId can be the Id of each tenant that signed up.