Finbuckle / Finbuckle.MultiTenant

Finbuckle.MultiTenant is an open-source multitenancy middleware library for .NET. It enables tenant resolution, per-tenant app behavior, and per-tenant data isolation.
https://www.finbuckle.com/multitenant
Apache License 2.0
1.26k stars 261 forks source link

Finbuckle.MultiTenant with Duende IdentityServer cannot login with per tenant ? #603

Open HDScarpe opened 1 year ago

HDScarpe commented 1 year ago

Currently I am setting up Finbuckle.MultiTenant with Duende IdentityServer. I'm getting an error when I log into the root account If I don't use .IsMultiTenant() then it can login as root account but can't login tenant account. This is my class design

HDScarpe commented 1 year ago

This is log error

System.NullReferenceException: Object reference not set to an instance of an object.
   at lambda_method343(Closure , QueryContext )
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsync[TResult](Expression query, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.ExecuteAsync[TResult](Expression expression, CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, Expression expression, Cancel
lationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.ExecuteAsync[TSource,TResult](MethodInfo operatorMethodInfo, IQueryable`1 source, LambdaExpression expression,
CancellationToken cancellationToken)
   at Microsoft.EntityFrameworkCore.EntityFrameworkQueryableExtensions.FirstOrDefaultAsync[TSource](IQueryable`1 source, Expression`1 predicate, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.EntityFrameworkCore.UserStore`9.FindByNameAsync(String normalizedUserName, CancellationToken cancellationToken)
   at Microsoft.AspNetCore.Identity.UserManager`1.FindByNameAsync(String userName)
   at Microsoft.AspNetCore.Identity.SignInManager`1.PasswordSignInAsync(String userName, String password, Boolean isPersistent, Boolean lockoutOnFailure)
   at IdentityProvider.Pages.Login.Index.OnPost() in D:\2022\.NET\eHosSystem\IdentityProvider\src\IdentityProvider\Pages\Account\Login\Index.cshtml.cs:line 96
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Convert[T](Object taskAsObject)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.ExecutorFactory.GenericTaskHandlerMethod.Execute(Object receiver, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeHandlerMethodAsync()
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeNextPageFilterAsync()
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Rethrow(PageHandlerExecutedContext context)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.RazorPages.Infrastructure.PageActionInvoker.InvokeInnerFilterAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeNextResourceFilter>g__Awaited|25_0(ResourceInvoker invoker, Task lastTask, State next, Scope scope, Object state,
 Boolean isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Rethrow(ResourceExecutedContextSealed context)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.InvokeFilterPipelineAsync()
--- End of stack trace from previous location ---
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ResourceInvoker.<InvokeAsync>g__Awaited|17_0(ResourceInvoker invoker, Task task, IDisposable scope)
   at Microsoft.AspNetCore.Routing.EndpointMiddleware.<Invoke>g__AwaitRequestTask|6_0(Endpoint endpoint, Task requestTask, ILogger logger)
   at Microsoft.AspNetCore.Authorization.AuthorizationMiddleware.Invoke(HttpContext context)
   at Duende.IdentityServer.Hosting.IdentityServerMiddleware.Invoke(HttpContext context, IEndpointRouter router, IUserSession userSession, IEventService events, IIssuerNameService is
suerNameService, ISessionCoordinationService sessionCoordinationService) in /_/src/IdentityServer/Hosting/IdentityServerMiddleware.cs:line 116
   at Duende.IdentityServer.Hosting.MutualTlsEndpointMiddleware.Invoke(HttpContext context, IAuthenticationSchemeProvider schemes) in /_/src/IdentityServer/Hosting/MutualTlsEndpointM
iddleware.cs:line 94
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
   at Duende.IdentityServer.Hosting.DynamicProviders.DynamicSchemeAuthenticationMiddleware.Invoke(HttpContext context) in /_/src/IdentityServer/Hosting/DynamicProviders/DynamicScheme
s/DynamicSchemeAuthenticationMiddleware.cs:line 47
   at Duende.IdentityServer.Hosting.BaseUrlMiddleware.Invoke(HttpContext context) in /_/src/IdentityServer/Hosting/BaseUrlMiddleware.cs:line 27
   at Finbuckle.MultiTenant.AspNetCore.MultiTenantMiddleware.Invoke(HttpContext context)
   at IdentityProvider.Extensions.ErrorWrappingMiddleware.Invoke(HttpContext context) in D:\2022\.NET\eHosSystem\IdentityProvider\src\IdentityProvider\Extensions\ErrorWrappingMiddlew
are.cs:line 23
hbulens commented 1 year ago

I'm experiencing the same problem, albeit with a different setup. The initial problem is a duplicate index:

InvalidOperationException: The indexes {'NormalizedUserName', 'TenantId'} on 'ApplicationUser' and {'NormalizedUserName'} on 'ApplicationUser' are both mapped to 'AspNetUsers.UserNameIndex', but with different columns ({'NormalizedUserName', 'TenantId'} and {'NormalizedUserName'}).

This can be addressed by adding some config in the DbContext's OnModelCreating override:

modelBuilder.Entity<ApplicationUser>()
.Metadata
.RemoveIndex(new[] { modelBuilder.Entity<ApplicationUser>().Property(u => u.NormalizedUserName).Metadata });

But that's where the above error message pops up. Specifically, in the ApplicationUserStore that's a child of UserStore<ApplicationUser>. Any time I want to invoke the Users property, the error as described here occurs.

My multi-tenancy setup is pretty simple:

 services
.AddMultiTenant<TenantInfo>()
.WithClaimStrategy()
.WithConfigurationStore();

There are some docs on the subject, but I can't fathom what should be done to solve this.

AndrewTriesToCode commented 1 year ago

@HDScarpe do you think you can recreate the issue in a test project with just ASP.NET Core Identity and taking IdentityServer out of the picture for a moment? Also can you tell me more about your IdentityContext and maybe post the class?

AndrewTriesToCode commented 1 year ago

Looking at code where the exception is, looks like its in the UserManager.GetByNameAsyc which is this code:

https://github.com/dotnet/aspnetcore/blob/51fc6824e30016404964fc96a4a098a764bdbbfa/src/Identity/EntityFrameworkCore/src/UserOnlyStore.cs#L245-L251

which boils down to:

Users.FirstOrDefaultAsync(u => u.NormalizedUserName == normalizedUserName, cancellationToken);

on the underlying db context. Just as a test, if you manually instantiate your dbcontext with a tenant and try the line above do you get the same error?

AndrewTriesToCode commented 1 year ago

@hbulens do you mind posting your dbcontext class or providing a repo that shows the error in action?

hbulens commented 1 year ago

Will try to provide an isolated case when I can get to it.

 public class MyDbContext : MultiTenantIdentityDbContext<ApplicationUser>
  {
      public MyDbContext (ITenantInfo tenantInfo, DbContextOptions options)
          : base(tenantInfo, options)
      {
          TenantNotSetMode = TenantNotSetMode.Overwrite;
      }

      protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
      {           
         // Breaking change in EF Core 7.0 to allow triggers
          configurationBuilder.Conventions.Add(_ => new BlankTriggerAddingConvention());
      }

      protected override void OnModelCreating(ModelBuilder modelBuilder)
      {
          base.OnModelCreating(modelBuilder);
          modelBuilder.Ignore<IdentityUserClaim<string>>();
          modelBuilder.Ignore<IdentityUserRole<string>>();
          modelBuilder.Ignore<IdentityUserLogin<string>>();
          modelBuilder.Ignore<IdentityRoleClaim<string>>();
          modelBuilder.Ignore<IdentityUserToken<string>>();
          modelBuilder.ApplyConfiguration(new CalendarMap());
          // ... ommitted the other configs          
      }
  }

Seems like we've been able to create a workaround by overriding UserClaimsPrincipalFactory.GenerateClaimsAsync and commenting out the following lines:

  //if (UserManager.SupportsUserClaim)
  //{
  //    id.AddClaims(await UserManager.GetClaimsAsync(user).ConfigureAwait(false));
  //}

That seems to work but it's a rather ugly solution, so we'll need to look into this a bit further.

JonasDev17 commented 9 months ago

@hbulens Did you manage to fix this? I have a very similar error:

https://stackoverflow.com/questions/77372700/c-sharp-asp-net-core-nullreferenceexception-when-using-multiple-authentication