如何动态设置 OpenIdConnect 中间件选项的权限?

Posted

技术标签:

【中文标题】如何动态设置 OpenIdConnect 中间件选项的权限?【英文标题】:How can I set the Authority on OpenIdConnect middleware options dynamically? 【发布时间】:2019-03-28 01:22:02 【问题描述】:

我们有多个租户,他们使用不同的权限(他们自己的,而不仅仅是标准提供商)。虽然我知道如何动态设置 clientId 和 secret,但我不知道如何设置权限。它在启动期间设置一次,之后就无法更改(或者看起来如此)。

由于我们有很多租户,我们不想在启动时全部注册,我们也不想在添加租户时要求重新启动。

有什么建议可以解决这个问题吗?我很想使用现有的中间件,但如果不可能,我可以自己编写。

感谢任何建议!

【问题讨论】:

使用带有 IdentityServer4 docs.identityserver.io/en/release/topics/… 的联合网关签出。这将允许一个可以处理外部身份验证或多租户要求的单一授权 (IdentityServer4)。 【参考方案1】:

虽然有点棘手,但绝对有可能。下面是一个简化示例,使用 MSFT OIDC 处理程序、自定义监视器和基于路径的租户解析:

实现您的租户解析逻辑。例如:

public class TenantProvider

    private readonly IHttpContextAccessor _httpContextAccessor;

    public TenantProvider(IHttpContextAccessor httpContextAccessor)
        => _httpContextAccessor = httpContextAccessor;

    public string GetCurrentTenant()
    
        // This sample uses the path base as the tenant.
        // You can replace that by your own logic.
        string tenant = _httpContextAccessor.HttpContext.Request.PathBase;
        if (string.IsNullOrEmpty(tenant))
        
            tenant = "default";
        

        return tenant;
    

public void Configure(IApplicationBuilder app)

    app.Use(next => context =>
    
        // This snippet uses a hardcoded resolution logic.
        // In a real world app, you'd want to customize that.
        if (context.Request.Path.StartsWithSegments("/fabrikam", out PathString path))
        
            context.Request.PathBase = "/fabrikam";
            context.Request.Path = path;
        

        return next(context);
    );

    app.UseAuthentication();

    app.UseMvc();

实现自定义IOptionsMonitor<OpenIdConnectOptions>:

public class OpenIdConnectOptionsProvider : IOptionsMonitor<OpenIdConnectOptions>

    private readonly ConcurrentDictionary<(string name, string tenant), Lazy<OpenIdConnectOptions>> _cache;
    private readonly IOptionsFactory<OpenIdConnectOptions> _optionsFactory;
    private readonly TenantProvider _tenantProvider;

    public OpenIdConnectOptionsProvider(
        IOptionsFactory<OpenIdConnectOptions> optionsFactory,
        TenantProvider tenantProvider)
    
        _cache = new ConcurrentDictionary<(string, string), Lazy<OpenIdConnectOptions>>();
        _optionsFactory = optionsFactory;
        _tenantProvider = tenantProvider;
    

    public OpenIdConnectOptions CurrentValue => Get(Options.DefaultName);

    public OpenIdConnectOptions Get(string name)
    
        var tenant = _tenantProvider.GetCurrentTenant();

        Lazy<OpenIdConnectOptions> Create() => new Lazy<OpenIdConnectOptions>(() => _optionsFactory.Create(name));
        return _cache.GetOrAdd((name, tenant), _ => Create()).Value;
    

    public IDisposable OnChange(Action<OpenIdConnectOptions, string> listener) => null;

实现自定义IConfigureNamedOptions&lt;OpenIdConnectOptions&gt;:

public class OpenIdConnectOptionsInitializer : IConfigureNamedOptions<OpenIdConnectOptions>

    private readonly IDataProtectionProvider _dataProtectionProvider;
    private readonly TenantProvider _tenantProvider;

    public OpenIdConnectOptionsInitializer(
        IDataProtectionProvider dataProtectionProvider,
        TenantProvider tenantProvider)
    
        _dataProtectionProvider = dataProtectionProvider;
        _tenantProvider = tenantProvider;
    

    public void Configure(string name, OpenIdConnectOptions options)
    
        if (!string.Equals(name, OpenIdConnectDefaults.AuthenticationScheme, StringComparison.Ordinal))
        
            return;
        

        var tenant = _tenantProvider.GetCurrentTenant();

        // Create a tenant-specific data protection provider to ensure
        // encrypted states can't be read/decrypted by the other tenants.
        options.DataProtectionProvider = _dataProtectionProvider.CreateProtector(tenant);

        // Other tenant-specific options like options.Authority can be registered here.
    

    public void Configure(OpenIdConnectOptions options)
        => Debug.Fail("This infrastructure method shouldn't be called.");

在您的 DI 容器中注册服务:

public void ConfigureServices(IServiceCollection services)

    // ...

    // Register the OpenID Connect handler.
    services.AddAuthentication()
        .AddOpenIdConnect();

    services.AddSingleton<TenantProvider>();
    services.AddSingleton<IOptionsMonitor<OpenIdConnectOptions>, OpenIdConnectOptionsProvider>();
    services.AddSingleton<IConfigureOptions<OpenIdConnectOptions>, OpenIdConnectOptionsInitializer>();

【讨论】:

抱歉回复晚了,花了一些时间让其他部分工作,但我想我有一个基于您的回复的工作示例。完成后我会发布完成的版本。谢谢! 这太棒了。我要添加的唯一建议是使用 app.UsePathBase(new PathString("/fabrikam"))。这使您可以在控制器中使用普通路由,因为它会忽略此前缀。 感谢 Kevin 提供的详细信息。根据您的方法,我已使其适用于 JwtBearer。您对 2 年后的实施还有其他想法吗?我无法为 ASP.NET Core 3.1 提出任何更好的解决方案。你怎么看?【参考方案2】:

Asp.NET Core 模型假定每个处理程序实例有一个上游权限。我的 Saml2 组件在一个处理程序中支持多个上游 Idp,当该假设不再成立时,它在系统的其余部分中存在缺陷。

在 Asp.NET Core 中,可以在运行时添加/删除提供程序,而无需重新启动。所以我建议找到一个基于此的模型。

如果您更希望一个处理程序可以具有按请求权限设置,我认为需要一个自定义处理程序 - Microsoft 的默认实现不支持。

【讨论】:

感谢您的输入安德斯,您总是非常乐于助人!我想我在用你的组件实现 SAML 支持后被宠坏了,我只是认为它会很简单 :D 我得到它的建议来自@Pinpoint

以上是关于如何动态设置 OpenIdConnect 中间件选项的权限?的主要内容,如果未能解决你的问题,请参考以下文章

OWIN OpenIdConnect 中间件 IDX10311 随机数无法验证

如何将 Azure OpenIdConnect OWIN 中间件 Cookie Auth 转换为 SPA 应用程序的 JavaScript JWT?

Microsoft OpenIdConnect Owin 响应

如何动态设置单选按钮的 'checked' 属性。

OpenIdConnect 错误 - 租户标识符可能不是空的 GUID

Asp.net Core OpenIdConnect (OIDC) 在哪里验证状态参数