Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to use Asp.Net Core Identity in Multi-Tenant environment

I have a working Asp.Net Core application with default Identity handling. Now I want to use it for multi domains. I extended ApplicationUser with DomainId. How can I handle not just username / email to authenticate / register the user, but also the current DomainId?

It's not a problem to get the current DomainId when the user is registering, logging into the system, I have a working multi-tenant Asp.Net Core system. I have issue only with user management with DomainId.

Is there any setting for this? What should I override to get this funcionality? For example UserStore, UserManager?

I found some tutorial for old Asp.Net Identity for example this: https://www.scottbrady91.com/ASPNET-Identity/Quick-and-Easy-ASPNET-Identity-Multitenancy But I couldn't find any tutorial for the new Asp.Net Core Identity.

like image 257
martonx Avatar asked Jan 07 '18 00:01

martonx


People also ask

What is multi-tenancy ASP NET core?

What is multi-tenancy? At its core, multi-tenancy is an architecture where one codebase serves multiple customers while maintaining data isolation. To customers, it feels like they have their own copy of the software running, while the application really is just one deployment.


1 Answers

Finally I figured it out. So first, I have to set user email to not unique. Sidenote: I'm using email for UserName also, I don't like to ask UserName from users:

services.Configure<IdentityOptions>(options =>
{
    options.User.RequireUniqueEmail = false;
});

When a new user register himself, I'm merging current Domain Id to UserName, this helps users to register with same Email / UserName into the system through totally different domains.

Then I had to create my custom UserManager, where I'm overriding FindByEmail:

using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MultiShop.Core.Repositories.User;
using MultiShop.Core.Tenant;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace Test
{
    public class MyShopUserManager<TUser> : UserManager<TUser>, IDisposable where TUser : class
{
    private readonly ITenantService tenantService;
    private readonly IUserRepository userRepository;

    public MyUserManager(IUserStore<TUser> store, IOptions<IdentityOptions> optionsAccessor,
        IPasswordHasher<TUser> passwordHasher, IEnumerable<IUserValidator<TUser>> userValidators,
        IEnumerable<IPasswordValidator<TUser>> passwordValidators, ILookupNormalizer keyNormalizer,
        IdentityErrorDescriber errors, IServiceProvider services, ILogger<UserManager<TUser>> logger,
        ITenantService tenantService, IUserRepository userRepository)
        : base(store, optionsAccessor, passwordHasher, userValidators, passwordValidators, keyNormalizer, errors, services, logger)
    {
        this.tenantService = tenantService;
        this.userRepository = userRepository;
    }

    public override async Task<TUser> FindByEmailAsync(string email)
    {
        ThrowIfDisposed();
        if (email == null)
        {
            throw new ArgumentNullException(nameof(email));
        }

        var users = (await userRepository.GetAllAsync()).Where(u => u.Email == email);

        if (users == null)
        {
            return null;
        }

        if (users.Count() == 1)
        {
            return await Store.FindByIdAsync(users.First().Id.ToString(), CancellationToken);
        }

        var currentDomain = tenantService.GetCurrentDomainAsync().Result;
        var user = users.SingleOrDefault(u => u.DomainId == currentDomain.Id);

        if (user == null)
        {
            return null;
        }

        return await Store.FindByIdAsync(user.Id.ToString(), CancellationToken);
    }
}
}

Be careful, because of multi-domains and generated UserNames, you should use userManager.FindByEmailAsync, instead of FindByNameAsync.

I had to create custom SignInManager for handling multi-domain users:

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MultiShop.Core.Tenant;
using MultiShop.Data.Entities;
using System.Threading.Tasks;

namespace Test
{
public class MySignInManager : SignInManager<ApplicationUser>

{
    private readonly ITenantService tenantService;

    public MySignInManager(UserManager<ApplicationUser> userManager, IHttpContextAccessor contextAccessor,
        IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory, IOptions<IdentityOptions> optionsAccessor,
        ILogger<SignInManager<ApplicationUser>> logger, IAuthenticationSchemeProvider schemes,
        ITenantService tenantService)
        : base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
    {
        this.tenantService = tenantService;
    }

    public override async Task<SignInResult> PasswordSignInAsync(string userName, string password, bool isPersistent, bool lockoutOnFailure)
    {
        var currentDomain = await tenantService.GetCurrentDomainAsync();
        return await base.PasswordSignInAsync($"{userName}{currentDomain.Id}", password, isPersistent, lockoutOnFailure);
    }
}
}

Finally I have to register my custom managers into Asp.Net Core Identity DI:

services
   .AddIdentity<ApplicationUser, ApplicationRole>()
   .AddEntityFrameworkStores<MultiShopDbContext>()
   .AddDefaultTokenProviders()
   //my custom managers for domain segmented users
   .AddUserManager<MyUserManager<ApplicationUser>>()
   .AddSignInManager<MySignInManager>();

That's it!

like image 99
martonx Avatar answered Sep 20 '22 23:09

martonx