We have multiple tenants, and they use different authorities (their own, not just standard providers). While I know how to dynamically set the clientId and secret, I can't figure out how to set the authority. It is set once, during startup, and afterwards it cannot be changed (or so it seems).
Since we have a lot of tenants we don't want to register all at startup, and we also don't want to require a restart when tenants are added.
Any suggestions how I can go about this? I'd love to use the existing middleware, but if it's not possible I could write my own.
Appreciate any suggestion!
While a bit tricky, it's definitely possible. Here's a simplified example, using the MSFT OIDC handler, a custom monitor and path-based tenant resolution:
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.");
}
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>();
}
The Asp.NET Core model assumes one upstream authority per handler instance. My Saml2 component supports multiple upstream Idps in one handler and it has drawbacks in the rest of the system when that assumption no longer is true.
In Asp.NET Core it is possible to add/remove providers at runtime, without requiring a restart. So I'd recommend finding a model based on that.
If you rather want one handler that can have a per-request Authority setting, I think that a custom handler is needed - Microsoft's default implementation won't support that.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With