Finbuckle.MultiTenant 路由策略和 IdentityServer4 租户登录页面重定向

ash*_*hin 5 identityserver4 asp.net-core-3.1

我们的目标是构建可以对多个租户进行身份验证的多租户身份 server4 应用程序。我们一直在尝试将Finbuckle Mutitenancy与 IdentityServer4集成来实现这一点。已经实施了文档中提到的路由策略的变体来解析每个请求的租户。当客户端请求 IdentityServer 的连接/授权端点(路由 URL 中没有租户标识符)时,身份服务器重定向到的登录 URL 会丢失租户标识符。自定义多租户策略成功执行并以预期方式设置租户信息:使用 http://localhost/Identity/Account/Login 而不是 http://localhost/tenant1/Account/Login。

我们已经提供了我们自己的IMul​​tiTenantStrategy实现来首先从路由中检索租户标识符,如果没有,则从 IdentityServer 的请求参数(自定义参数,从请求令牌的客户端添加的“租户”。)。该MultiTenantMiddleware成功地使用自定义的策略获取承包者标识符,并设置必要的tenantInfo,存储策略等。但随后的连接/授权端点重定向到http://本地主机/身份/帐号/登录跳过租户模板。

但是,如果我们使用FallbackStrategy,它定义了静态租户标识符以在所有其他策略失败时使用,那么授权端点会将浏览器重定向到 http://localhost/tenant1/Account/Login,前提是在 FallbackStrategy 中使用了tenant1静态标识符。这是代码片段:

IdentityServer 的配置服务:

    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDbContext<ApplicationDbContext>();
        services.AddDefaultIdentity<IdentityUser>()
            .AddEntityFrameworkStores<ApplicationDbContext>();

        services.AddControllersWithViews().AddRazorRuntimeCompilation();
        services.AddRazorPages(options =>
        {
            // Since we are using the route multitenant strategy we must add the
            // route parameter to the Pages conventions used by Identity.
            options.Conventions.AddAreaFolderRouteModelConvention("Identity", "/Account", model =>
            {
                foreach (var selector in model.Selectors)
                {
                    selector.AttributeRouteModel.Template =
                        AttributeRouteModel.CombineTemplates("{__tenant__}", selector.AttributeRouteModel.Template);
                }
            });
        });

        services.DecorateService<LinkGenerator, AmbientValueLinkGenerator>(new List<string> { "__tenant__" });

        services.AddMultiTenant()
                .WithStrategy<CustomMultiTenantStrategy>(ServiceLifetime.Transient, "__tenant__") // Looks at route first then specific to idserver4's request
                //.WithFallbackStrategy("tenant1") // If added, always redirects to tenant1's login page.
                .WithConfigurationStore()
                .WithRemoteAuthentication()
                .WithPerTenantOptions<AuthenticationOptions>((options, tenantInfo) =>
                {
                    // Allow each tenant to have a different default challenge scheme.
                    if (tenantInfo.Items.TryGetValue("ChallengeScheme", out object challengeScheme))
                    {
                        options.DefaultChallengeScheme = (string)challengeScheme;
                        // options.DefaultSignOutScheme = (string)challengeScheme;
                    }
                })
                .WithPerTenantOptions<CookieAuthenticationOptions>((options, tenantInfo) =>
                {
                    // Since we are using the route strategy configure each tenant
                    // to have a different cookie name and adjust the paths.
                    options.Cookie.Path = $"/{tenantInfo.Identifier}";
                    options.Cookie.Name = $"{tenantInfo.Id}_authentication";
                    options.LoginPath = $"{options.Cookie.Path}{options.LoginPath}";
                    options.LogoutPath = $"{options.Cookie.Path}{options.LogoutPath}";
                });

        // configure identity server with in-memory stores, keys, clients and scopes
        services.AddIdentityServer()
            .AddDeveloperSigningCredential()
            .AddInMemoryPersistedGrants()
            .AddInMemoryIdentityResources(ResourceStore.GetIdentityResources())
            .AddInMemoryApiResources(ResourceStore.GetApiResources())
            .AddInMemoryClients(ClientStore.Get())
            .AddAspNetIdentity<IdentityUser>()
            .AddProfileService<CustomProfileService>();
    }
Run Code Online (Sandbox Code Playgroud)

IdentityServer 的 Configure 方法:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
        if (env.IsDevelopment())
        {
            app.UseDeveloperExceptionPage();
            app.UseDatabaseErrorPage();
        }
        else
        {
            app.UseExceptionHandler("/Home/Error");
        }

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

        app.UseEndpoints(endpoints =>
        {
            endpoints.MapControllerRoute("default", "{__tenant__=}/{controller=Home}/{action=Index}");
            endpoints.MapRazorPages();
        });
    }
Run Code Online (Sandbox Code Playgroud)

自定义多租户策略:

public class CustomMultiTenantStrategy : IMultiTenantStrategy
{
    internal readonly string tenantParam;
    private readonly IIdentityServerInteractionService _interactionService;
    public CustomMultiTenantStrategy(string tenantParam, IIdentityServerInteractionService interactionService)
    {
        if (string.IsNullOrWhiteSpace(tenantParam))
        {
            throw new ArgumentException($"\"{nameof(tenantParam)}\" must not be null or whitespace", nameof(tenantParam));
        }
        this.tenantParam = tenantParam;
        this._interactionService = interactionService;
    }

    public async Task<string> GetIdentifierAsync(object context)
    {
        if (!(context is HttpContext))
            throw new MultiTenantException(null,
                new ArgumentException($"\"{nameof(context)}\" type must be of type HttpContext", nameof(context)));

        var httpContext = context as HttpContext;

        object identifier = null;
        httpContext.Request.RouteValues.TryGetValue(tenantParam, out identifier);

        // Fallback to read from the request query params
        if (identifier == null)
        {
            httpContext.Request.Query.TryGetValue("tenant", out StringValues tenant);

            if (tenant.Count > 0)
            {
                identifier = tenant.ToString();
            }
            else // authorize/connect request would go in here
            {
                if (httpContext.Request.Query.ContainsKey("returnUrl"))
                {
                    var returnUrl = httpContext.Request.Query["returnUrl"];

                    var authContext = await this._interactionService.GetAuthorizationContextAsync(returnUrl);

                    identifier = authContext.Parameters["tenant"];
                }
            }
        }

        return await Task.FromResult(identifier as string);
    }
 }
Run Code Online (Sandbox Code Playgroud)

MVC 客户端的配置服务:

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = "Cookies";
            options.DefaultChallengeScheme = "oidc";
        })
            .AddCookie("Cookies")
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = "http://localhost";
                options.RequireHttpsMetadata = false;
                options.ClientId = "test-ui";
                options.ClientSecret = "XXXXX";
                options.ResponseType = "code id_token";
                options.Scope.Add("custom-profile");
                options.Scope.Add("offline_access");
                options.SaveTokens = true;

                options.Events = new OpenIdConnectEvents
                {
                    OnRedirectToIdentityProvider = (ctx) =>
                    {
                        var tenant = ctx.HttpContext.Request.IsHttps ? "tenant1" : "tenant2"; // Temporary way to swtich between two tenants - the same mvc client is used by different tenants, in prod, it would be identified based on subdomain.
                        ctx.ProtocolMessage.Parameters.Add("tenant", tenant );
                        ctx.ProtocolMessage.AcrValues = $"tenant:{tenant}";

                        return Task.CompletedTask;
                    }
                };
            });

        services.AddControllersWithViews();
Run Code Online (Sandbox Code Playgroud)

如果我们从 ConfigureServices 中删除 .WithFallbackStrategy("tenant1"),那么身份服务器将重定向到 http://localhost/Identity/Account/Login 导致 404。我们希望租户名称根据参数动态设置MVC 客户端通过请求参数传递。任何熟悉 Finbuckle.MultiTenant 的人都可以对此有所了解吗?