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.32k stars 267 forks source link

NullReferenceException while trying to register a user using the default Identity API #790

Closed EvanBor closed 8 months ago

EvanBor commented 8 months ago

Hello! I'm trying to integrate your libraries into a minimalistic API based on standard .NET solutions, but I came across an error that occurs when registering a user.

System.NullReferenceException: Object reference not set to an instance of an object.
         at lambda_method33(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, CancellationToken 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.UserOnlyStore`6.FindByNameAsync(String normalizedUserName, CancellationToken cancellationToken)
         at Microsoft.AspNetCore.Identity.UserManager`1.FindByNameAsync(String userName)
         at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateUserName(UserManager`1 manager, TUser user)
         at Microsoft.AspNetCore.Identity.UserValidator`1.ValidateAsync(UserManager`1 manager, TUser user)
         at Microsoft.AspNetCore.Identity.UserManager`1.ValidateUserAsync(TUser user)
         at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user)
         at Microsoft.AspNetCore.Identity.UserManager`1.CreateAsync(TUser user, String password)
         at Microsoft.AspNetCore.Routing.IdentityApiEndpointRouteBuilderExtensions.<>c__DisplayClass1_0`1.<<MapIdentityApi>b__0>d.MoveNext()

I can’t say for sure that this is a bug or my mistake, but please point me to a possible solution.

using Finbuckle.MultiTenant;
using Microsoft.EntityFrameworkCore;
using Microsoft.OpenApi.Models;
using ShowcaseBackend.Contexts;
using ShowcaseBackend.Models;
using Swashbuckle.AspNetCore.Filters;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    options.AddSecurityDefinition("Auth", new OpenApiSecurityScheme
    {
        In = ParameterLocation.Header,
        Name = "Authorization",
        Type = SecuritySchemeType.ApiKey
    });
    options.OperationFilter<SecurityRequirementsOperationFilter>();
});

builder.Services.AddDbContext<MultiTenantStoreDataContext>(
    options => _ = builder.Configuration.GetValue("Provider", "Sqlite") switch
    {
        "Sqlite" => options.UseSqlite(
            builder.Configuration.GetConnectionString("Sqlite")
        ),

        _ => throw new Exception($"Unsupported provider: {builder.Configuration.GetValue("Provider", "Sqlite")}")
    });

builder.Services.AddDbContext<DataContext>(
    options => _ = builder.Configuration.GetValue("Provider", "Sqlite") switch
    {
        "Sqlite" => options.UseSqlite(
            builder.Configuration.GetConnectionString("Sqlite")
        ),

        _ => throw new Exception($"Unsupported provider: {builder.Configuration.GetValue("Provider", "Sqlite")}")
    });

builder.Services.AddAuthorization();
builder.Services.AddMultiTenant<TenantInfo>()
    .WithDelegateStrategy(async context =>
    {
        if (context is not HttpContext httpContext)
            return "DefaultTenant";

        httpContext.Request.Headers.TryGetValue("Tenant", out var tenantIdParam);
        var tenantFromHeader = tenantIdParam.ToString();
        var finalTenant = string.IsNullOrEmpty(tenantFromHeader) ? "DefaultTenant" : tenantFromHeader;
        return finalTenant;
    })
    .WithEFCoreStore<MultiTenantStoreDataContext, TenantInfo>();
builder.Services.AddIdentityApiEndpoints<User>()
    .AddEntityFrameworkStores<DataContext>();

var app = builder.Build();

if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();

    if (app.Configuration.GetValue("AutoMigrate", false))
    {
        Console.ForegroundColor = ConsoleColor.DarkYellow;
        Console.WriteLine("--- Auto-migrate start ---");
        Console.ResetColor();
        using (var scope = app.Services.CreateScope())
        {
            Task.WaitAll(
                scope.ServiceProvider.GetRequiredService<MultiTenantStoreDataContext>().Database.MigrateAsync(),
                scope.ServiceProvider.GetRequiredService<DataContext>().Database.MigrateAsync()
            );
        }
        Console.ForegroundColor = ConsoleColor.DarkYellow;
        Console.WriteLine("--- Auto-migrate end ---");
        Console.ResetColor();
    }
}

app.UseHttpsRedirection();

app.UseAuthorization();

app.UseMultiTenant();

app.MapGroup("/auth").MapIdentityApi<User>();

app.Run();
AndrewTriesToCode commented 8 months ago

Hi, I suspect the issue is that UseAuthentication is triggering an EFCore query somewhere internally which wants a tenant, but since UseMultiTenant middleware hasn’t run yet it does know the tenant.

If it is because it is trying to do security stamp validation I have a workaround built in for this if you use WithPerTenantAuthentication as described in the docs.

Otherwise try moving the tenant middleware to occur before the authentication middleware. Let me know if this helps.

EvanBor commented 8 months ago

Thanks a lot! Using a MultiTenant middleware before the authorization middleware helped solve my problem.