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
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(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 : UserManager, IDisposable where TUser : class
{
private readonly ITenantService tenantService;
private readonly IUserRepository userRepository;
public MyUserManager(IUserStore store, IOptions optionsAccessor,
IPasswordHasher passwordHasher, IEnumerable> userValidators,
IEnumerable> passwordValidators, ILookupNormalizer keyNormalizer,
IdentityErrorDescriber errors, IServiceProvider services, ILogger> 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 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
{
private readonly ITenantService tenantService;
public MySignInManager(UserManager userManager, IHttpContextAccessor contextAccessor,
IUserClaimsPrincipalFactory claimsFactory, IOptions optionsAccessor,
ILogger> logger, IAuthenticationSchemeProvider schemes,
ITenantService tenantService)
: base(userManager, contextAccessor, claimsFactory, optionsAccessor, logger, schemes)
{
this.tenantService = tenantService;
}
public override async Task 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()
.AddEntityFrameworkStores()
.AddDefaultTokenProviders()
//my custom managers for domain segmented users
.AddUserManager>()
.AddSignInManager();
That's it!