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.33k stars 264 forks source link

Duende Identity Server Base Path Strategy #625

Open goforebroke opened 1 year ago

goforebroke commented 1 year ago

Andrew,

I have read several of the issues regarding Duende Identity Server 6 and Finbuckle. In most of the issues, you recommend using the base path strategy (Use of claim strategy and Identity types #613). I have tried to set this up, but I keep getting a 404 error when navigating to the login page with the tenant identifier.

Here is how things are setup in with Identity server.

public static WebApplication ConfigureServices(this WebApplicationBuilder builder)
 {
            builder.Services.AddRazorPagesServices();
            builder.Services.AddIdentityServices(builder.Configuration);
            builder.Services.AddMultiTenantServices(builder.Configuration); //Configure Finbuckle components
            builder.Services.AddMessagingServices(builder.Configuration);

            return builder.Build();
 }

IdentityServer configuration below. For now I am using in memory clients, scopes and resources for development purposes

static IServiceCollection AddIdentityServices(this IServiceCollection services, IConfiguration configuration)
{
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlite(configuration.GetConnectionString("IdentityConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>(options =>
            {
                options.User.RequireUniqueEmail = true;
            })
            .AddEntityFrameworkStores<ApplicationDbContext>()
            .AddDefaultTokenProviders();

            services
                .AddIdentityServer(options =>
                {
                    options.Events.RaiseErrorEvents = true;
                    options.Events.RaiseInformationEvents = true;
                    options.Events.RaiseFailureEvents = true;
                    options.Events.RaiseSuccessEvents = true;

                    // see https://docs.duendesoftware.com/identityserver/v6/fundamentals/resources/
                    options.EmitStaticAudienceClaim = true;
                })
                .AddInMemoryClients(Config.Clients)
                .AddInMemoryApiScopes(Config.ApiScopes)
                .AddInMemoryIdentityResources(Config.IdentityResources)
                .AddAspNetIdentity<ApplicationUser>()
                .AddProfileService<CustomProfileService>();

            return services;
}

EFCoreStore below

static IServiceCollection AddMultiTenantServices(this IServiceCollection services,
            IConfiguration configuration)
{
            services.AddDbContext<TenantDbContext>(options =>
                options.UseSqlite(configuration.GetConnectionString("TenantConnection")))
                .AddMultiTenant<MyTenant>()
                .WithEFCoreStore<TenantDbContext, MyTenant>()
                .WithBasePathStrategy(options => 
                {
                    options.RebaseAspNetCorePathBase = true;
                });

            return services;
}

Below is how the pipline is set up

public static WebApplication ConfigurePipeline(this WebApplication app)
{
            app.UseSerilogRequestLogging();

            if (app.Environment.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            app.UseStaticFiles();
            app.UseRouting();
            app.UseIdentityServer();
            app.UseMultiTenant();
            app.UseAuthorization();
            app.MapRazorPages();

            return app;
}

Below are my two EF DB Contexts. One for Identity and the other for the EFCore Store

Identity...

public class ApplicationDbContext : MultiTenantIdentityDbContext<ApplicationUser>
{
    public ApplicationDbContext(ITenantInfo tenantInfo) : base(tenantInfo)
    {
    }
    public ApplicationDbContext(ITenantInfo tenantInfo, 
        DbContextOptions<ApplicationDbContext> options) : base(tenantInfo, options)
    {
    }
    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        base.OnConfiguring(optionsBuilder);
    }
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);

        // Customize the ASP.NET Identity model and override the defaults if needed.
        // For example, you can rename the ASP.NET Identity table names and more.
        // Add your customizations after calling base.OnModelCreating(builder);

        builder.ApplyConfiguration(new ApplicationUserConfiguration());
    }
}

Store..

public class TenantDbContext : EFCoreStoreDbContext<MyTenant>
{
        public TenantDbContext(DbContextOptions<TenantDbContext> options) : base(options)
        {
        }
        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);

            builder.ApplyConfiguration(new TenantConfiguration());

        }
}

When I navigate to https://localhost:5001/Goforebroke/Account/Login, I get a 404 error.

My serilog log shows that Finbuckle discovers the tenant, but the URL is not found.

[10:46:09 Debug] Finbuckle.MultiTenant.Stores.EFCoreStore
TryGetByIdentifierAsync: Tenant found with identifier "Goforebroke"

[10:46:09 Information] Serilog.AspNetCore.RequestLoggingMiddleware
HTTP GET /Account/Login responded 404 in 184.8214 ms

Have set things up in the correct order, or am I missing something my setup?

goforebroke commented 1 year ago

After a little more research, reading , and looking at your sample with Identity Server 4,I adjusted the order where I add "UseMultiTenant()". I have things working now

AndrewTriesToCode commented 1 year ago

Hi there, glad you got it working and sorry for the slow reply. Nice to hear from you!

natelaff commented 1 year ago

@goforebroke did you get this rolling? i am removing base path strategy while i'm updating identity server to use razor pages instead of controllers.

its like you have to get the acr tenant then add it to a temporary claim almost immediately so that it tracks through the rest of the flow? i am not always able to get the acr values tenant because that query string with returnUrl doesn't always have it.

goforebroke commented 1 year ago

@natelaff sorry I have taken so long to get back to you. I have been super busy with my regular job outside of this personal project.

I have two Identity server clients, both razor applications. One client uses a static strategy (default tenant) and the other uses a base path strategy . The second client has an area where a user "sets" their tenant. My identity server is also configured to use a base path strategy. I don't pass the tenant acr value through from the clients to Identity Server. The tenant is always passed in the URL as part of the authorization/authentication request. In addition, I use a custom RedirectUriValidator in Identity server to ensure that the registered "RedirectUris" for the identity clients validate. I probably have not got far enough into my project to encounter real problems. Is there something in my setup that you would like to see?