adn*_*ili 6 openid-connect asp.net-core openiddict
我有一个多租户(单一数据库)应用程序,允许跨不同租户使用相同的用户名/电子邮件.
在登录时(隐含流程)我如何识别租户?我想到了以下可能性:
在注册时索要帐户的用户slug(公司/租户蛞蝓)和登录用户期间应提供slug沿username和password.
但是在open id请求中没有参数来发送slug.
OAuth在注册时创建一个应用程序并slug用作client_id.在登录传入slug时client_id,我将用于获取租户ID并继续进一步验证用户.
这种做法好吗?
编辑:
也尝试制作路线param的slug部分
.EnableTokenEndpoint("/connect/{slug}/token");
Run Code Online (Sandbox Code Playgroud)
但openiddict不支持这一点.
Kév*_*let 12
由麦圭尔建议的做法将与OpenIddict工作(您可以访问acr_values通过属性OpenIdConnectRequest.AcrValues),但它不是推荐的选项(这不是从安全的角度看理想:由于发行方是所有租户一样,他们最终共享相同的签名键).
相反,考虑为每个租户运行一个发行者.为此,您至少有两个选择:
尝试给OrchardCore的OpenID模块:它基于OpenIddict并且本机支持多租户.它仍处于测试阶段,但它正在积极开发中.
覆盖OpenIddict使用的选项监视器以使用每个租户选项.
以下是第二个选项的简化示例,使用自定义监视器和基于路径的租户解析:
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;
}
}
Run Code Online (Sandbox Code Playgroud)
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();
}
Run Code Online (Sandbox Code Playgroud)
IOptionsMonitor<OpenIddictServerOptions>:public class OpenIddictServerOptionsProvider : IOptionsMonitor<OpenIddictServerOptions>
{
private readonly ConcurrentDictionary<(string name, string tenant), Lazy<OpenIddictServerOptions>> _cache;
private readonly IOptionsFactory<OpenIddictServerOptions> _optionsFactory;
private readonly TenantProvider _tenantProvider;
public OpenIddictServerOptionsProvider(
IOptionsFactory<OpenIddictServerOptions> optionsFactory,
TenantProvider tenantProvider)
{
_cache = new ConcurrentDictionary<(string, string), Lazy<OpenIddictServerOptions>>();
_optionsFactory = optionsFactory;
_tenantProvider = tenantProvider;
}
public OpenIddictServerOptions CurrentValue => Get(Options.DefaultName);
public OpenIddictServerOptions Get(string name)
{
var tenant = _tenantProvider.GetCurrentTenant();
Lazy<OpenIddictServerOptions> Create() => new Lazy<OpenIddictServerOptions>(() => _optionsFactory.Create(name));
return _cache.GetOrAdd((name, tenant), _ => Create()).Value;
}
public IDisposable OnChange(Action<OpenIddictServerOptions, string> listener) => null;
}
Run Code Online (Sandbox Code Playgroud)
IConfigureNamedOptions<OpenIddictServerOptions>:public class OpenIddictServerOptionsInitializer : IConfigureNamedOptions<OpenIddictServerOptions>
{
private readonly IDataProtectionProvider _dataProtectionProvider;
private readonly TenantProvider _tenantProvider;
public OpenIddictServerOptionsInitializer(
IDataProtectionProvider dataProtectionProvider,
TenantProvider tenantProvider)
{
_dataProtectionProvider = dataProtectionProvider;
_tenantProvider = tenantProvider;
}
public void Configure(string name, OpenIddictServerOptions options) => Configure(options);
public void Configure(OpenIddictServerOptions options)
{
var tenant = _tenantProvider.GetCurrentTenant();
// Create a tenant-specific data protection provider to ensure authorization codes,
// access tokens and refresh tokens can't be read/decrypted by the other tenants.
options.DataProtectionProvider = _dataProtectionProvider.CreateProtector(tenant);
// Other tenant-specific options can be registered here.
}
}
Run Code Online (Sandbox Code Playgroud)
public void ConfigureServices(IServiceCollection services)
{
// ...
// Register the OpenIddict services.
services.AddOpenIddict()
.AddCore(options =>
{
// Register the Entity Framework stores.
options.UseEntityFrameworkCore()
.UseDbContext<ApplicationDbContext>();
})
.AddServer(options =>
{
// Register the ASP.NET Core MVC binder used by OpenIddict.
// Note: if you don't call this method, you won't be able to
// bind OpenIdConnectRequest or OpenIdConnectResponse parameters.
options.UseMvc();
// Note: the following options are registered globally and will be applicable
// to all the tenants. They can be overridden from OpenIddictServerOptionsInitializer.
options.AllowAuthorizationCodeFlow();
options.EnableAuthorizationEndpoint("/connect/authorize")
.EnableTokenEndpoint("/connect/token");
options.DisableHttpsRequirement();
});
services.AddSingleton<TenantProvider>();
services.AddSingleton<IOptionsMonitor<OpenIddictServerOptions>, OpenIddictServerOptionsProvider>();
services.AddSingleton<IConfigureOptions<OpenIddictServerOptions>, OpenIddictServerOptionsInitializer>();
}
Run Code Online (Sandbox Code Playgroud)
要确认这是否正常,请导航到http:// localhost:[port] /fabrikam/.well-known/openid-configuration(您应该使用OpenID Connect元数据获得JSON响应).
您的 OAuth 流程走在正确的轨道上。当您在客户端 Web 应用程序的启动代码中注册 OpenID Connect 方案时,请添加事件处理程序OnRedirectToIdentityProvider,并使用该处理程序将“slug”值添加为“租户”ACR 值(OIDC 称之为“身份验证上下文类参考”) 。
以下是如何将其传递到服务器的示例:
.AddOpenIdConnect("tenant", options =>
{
options.CallbackPath = "/signin-tenant";
// other options omitted
options.Events = new OpenIdConnectEvents
{
OnRedirectToIdentityProvider = async context =>
{
string slug = await GetCurrentTenantAsync();
context.ProtocolMessage.AcrValues = $"tenant:{slug}";
}
};
}
Run Code Online (Sandbox Code Playgroud)
您没有指定这将是哪种类型的服务器,但 ACR(和“租户”值)是 OIDC 的标准部分。如果您使用的是 Identity Server 4,您只需将交互服务注入到处理登录的类中并读取属性Tenant,该属性会自动从 ACR 值中为您解析出来。由于多种原因,此示例是无效代码,但它演示了重要部分:
public class LoginModel : PageModel
{
private readonly IIdentityServerInteractionService interaction;
public LoginModel(IIdentityServerInteractionService interaction)
{
this.interaction = interaction;
}
public async Task<IActionResult> PostEmailPasswordLoginAsync()
{
var context = await interaction.GetAuthorizationContextAsync(returnUrl);
if(context != null)
{
var slug = context.Tenant;
// etc.
}
}
}
Run Code Online (Sandbox Code Playgroud)
在识别个人用户帐户方面,如果您坚持使用“主题 ID”作为唯一用户 ID 的 OIDC 标准,您的生活将会容易得多。(换句话说,将其作为存储用户数据的键,例如租户“slug”、用户电子邮件地址、密码盐和哈希值等)
对于任何对 Kevin Chalet 接受的答案的替代方法(更多的是扩展)感兴趣的人,请查看此处描述的模式,使用 https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs 的自IOptions<TOption>定义MultiTenantOptionsManager<TOptions> 实现/选项.md
相同模式的身份验证示例位于https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/master/docs/Authentication.md
技巧是使用IOptionsMonitorCache租户感知的自定义并始终返回租户范围的结果https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsCache.cs
internal class MultiTenantOptionsManager<TOptions> : IOptions<TOptions>, IOptionsSnapshot<TOptions> where TOptions : class, new()
{
private readonly IOptionsFactory<TOptions> _factory;
private readonly IOptionsMonitorCache<TOptions> _cache; // Note: this is a private cache
/// <summary>
/// Initializes a new instance with the specified options configurations.
/// </summary>
/// <param name="factory">The factory to use to create options.</param>
public MultiTenantOptionsManager(IOptionsFactory<TOptions> factory, IOptionsMonitorCache<TOptions> cache)
{
_factory = factory;
_cache = cache;
}
public TOptions Value
{
get
{
return Get(Microsoft.Extensions.Options.Options.DefaultName);
}
}
public virtual TOptions Get(string name)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
// Store the options in our instance cache.
return _cache.GetOrAdd(name, () => _factory.Create(name));
}
public void Reset()
{
_cache.Clear();
}
}
Run Code Online (Sandbox Code Playgroud)
public class MultiTenantOptionsCache<TOptions> : IOptionsMonitorCache<TOptions> where TOptions : class
{
private readonly IMultiTenantContextAccessor multiTenantContextAccessor;
// The object is just a dummy because there is no ConcurrentSet<T> class.
//private readonly ConcurrentDictionary<string, ConcurrentDictionary<string, object>> _adjustedOptionsNames =
// new ConcurrentDictionary<string, ConcurrentDictionary<string, object>>();
private readonly ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>> map = new ConcurrentDictionary<string, IOptionsMonitorCache<TOptions>>();
public MultiTenantOptionsCache(IMultiTenantContextAccessor multiTenantContextAccessor)
{
this.multiTenantContextAccessor = multiTenantContextAccessor ?? throw new ArgumentNullException(nameof(multiTenantContextAccessor));
}
/// <summary>
/// Clears all cached options for the current tenant.
/// </summary>
public void Clear()
{
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
cache.Clear();
}
/// <summary>
/// Clears all cached options for the given tenant.
/// </summary>
/// <param name="tenantId">The Id of the tenant which will have its options cleared.</param>
public void Clear(string tenantId)
{
tenantId = tenantId ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
cache.Clear();
}
/// <summary>
/// Clears all cached options for all tenants and no tenant.
/// </summary>
public void ClearAll()
{
foreach(var cache in map.Values)
cache.Clear();
}
/// <summary>
/// Gets a named options instance for the current tenant, or adds a new instance created with createOptions.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="createOptions">The factory function for creating the options instance.</param>
/// <returns>The existing or new options instance.</returns>
public TOptions GetOrAdd(string name, Func<TOptions> createOptions)
{
if (createOptions == null)
{
throw new ArgumentNullException(nameof(createOptions));
}
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.GetOrAdd(name, createOptions);
}
/// <summary>
/// Tries to adds a new option to the cache for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <param name="options">The options instance.</param>
/// <returns>True if the options was added to the cache for the current tenant.</returns>
public bool TryAdd(string name, TOptions options)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.TryAdd(name, options);
}
/// <summary>
/// Try to remove an options instance for the current tenant.
/// </summary>
/// <param name="name">The options name.</param>
/// <returns>True if the options was removed from the cache for the current tenant.</returns>
public bool TryRemove(string name)
{
name = name ?? Microsoft.Extensions.Options.Options.DefaultName;
var tenantId = multiTenantContextAccessor.MultiTenantContext?.TenantInfo?.Id ?? "";
var cache = map.GetOrAdd(tenantId, new OptionsCache<TOptions>());
return cache.TryRemove(name);
}
}
Run Code Online (Sandbox Code Playgroud)
优点是您不必扩展每种类型的IOption<TOption>.
private static MultiTenantOptionsManager<TOptions> BuildOptionsManager<TOptions>(IServiceProvider sp) where TOptions : class, new()
{
var cache = ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsCache<TOptions>));
return (MultiTenantOptionsManager<TOptions>)
ActivatorUtilities.CreateInstance(sp, typeof(MultiTenantOptionsManager<TOptions>), new[] { cache });
}
Run Code Online (Sandbox Code Playgroud)
public static void WithPerTenantOptions<TOptions>(Action<TOptions, TenantInfo> tenantInfo) where TOptions : class, new()
{
// Other required services likes custom options factory, see the linked example above for full code
Services.TryAddScoped<IOptionsSnapshot<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
Services.TryAddSingleton<IOptions<TOptions>>(sp => BuildOptionsManager<TOptions>(sp));
}
Run Code Online (Sandbox Code Playgroud)
每次IOptions<TOption>.Value调用时,它都会查找多租户感知缓存来检索它。IAuthenticationSchemeProvider所以你也可以在单例中方便地使用它。
OpenIddictServerOptionsProvider现在,您可以注册与接受的答案相同的租户特定选项。
| 归档时间: |
|
| 查看次数: |
3561 次 |
| 最近记录: |