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

21
我们有多个租户,他们使用不同的身份验证机构(自己的机构,而不仅仅是标准提供商)。虽然我知道如何动态设置clientId和secret,但我无法弄清楚如何设置身份验证机构。它在启动时设置一次,之后就无法更改(至少看起来是这样)。
由于我们有很多租户,我们不想在启动时注册所有租户,也不想要求添加租户时重新启动。
有什么建议吗?我很想使用现有的中间件,但如果不可能,我可以编写自己的中间件。
感谢任何建议!

使用IdentityServer4的联合网关进行结账,详情请参见http://docs.identityserver.io/en/release/topics/federation_gateway.html。这将允许一个单一的权威(IdentityServer4)处理外部身份验证或多租户要求。 - Brad
2个回答

25
虽然有点棘手,但是这绝对是可行的。以下是一个使用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<OpenIdConnectOptions>

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>();
}

抱歉回复晚了,花了一些时间让其他部分正常运作,但我认为我已经根据您的回复有一个可行的示例。完成后我会尽快发布最终版本。谢谢! - Iris Classon
1
感谢提供详细的示例,Kevin。我已经按照你的方法使JwtBearer正常工作了。 在两年后的实现方面,您有其他想法吗?对于ASP.NET Core 3.1,我无法想出更好的解决方案。您认为呢? - SerjG
我正在尝试做类似的事情,我的租户只是从用户会话中读取,并且因此可以在运行时更改为个别用户。我将租户ID设置为“scope”列表的一部分,但我真的无法弄清如何在运行时使用不同的配置请求新令牌? - aweis
1
我想赞同@SerjG的评论 - 鉴于Asp.Net 6.0,我们是否知道这仍然是在.Net 6.0中获取多租户JwtBearer身份验证的最佳方法? - Brian Mikinski
在.NET 7中,这个方法不再起作用,在OpenIdConnectOptionsProvider中,_optionsFactory.Create(name)调用将通过调用OpenIdConnectOptions.Validate()方法抛出验证异常,因为它缺少有效的CliendId参数。https://github.com/dotnet/aspnetcore/blob/15e9bd26449b17ec0677f4e353b74507832f4bad/src/Security/Authentication/OpenIdConnect/src/OpenIdConnectOptions.cs#L81 - Alex
显示剩余4条评论

1
Asp.NET Core模型假设每个处理程序实例只有一个上游授权方。我的Saml2组件支持一个处理程序中的多个上游Idps,当该假设不再成立时,它在系统的其余部分中具有缺点。
在Asp.NET Core中,可以在运行时添加/删除提供程序,而无需重新启动。因此,我建议找到基于此的模型。
如果您更喜欢一个可以具有每个请求授权设置的处理程序,则需要自定义处理程序-微软的默认实现将不支持此功能。

1
谢谢您的输入,Anders,您总是非常有帮助!我想在使用您的组件实现SAML支持后,我已经被宠坏了,我只是认为这将是同样容易的:D根据@Pinpoint的建议,我已经让它工作了。 - Iris Classon

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接