openid connect - 在登录期间识别租户

adn*_*ili 6 openid-connect asp.net-core openiddict

我有一个多租户(单一数据库)应用程序,允许跨不同租户使用相同的用户名/电子邮件.

在登录时(隐含流程)我如何识别租户?我想到了以下可能性:

  1. 在注册时索要帐户的用户slug(公司/租户蛞蝓)和登录用户期间应提供slug沿usernamepassword.

    但是在open id请求中没有参数来发送slug.

  2. OAuth在注册时创建一个应用程序并slug用作client_id.在登录传入slugclient_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)

在DI容器中注册服务:

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响应).

  • 我没有修改 OptionsMonitor,而是修改了 OptionsCache 以感知租户。因此,我可以使 IOptions 和 OptionsMonitor 成为多租户 (2认同)

McG*_*V10 6

您的 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”、用户电子邮件地址、密码盐和哈希值等)


Das*_*jes 5

对于任何对 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

实现的完整源代码位于https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/7bc72692b0f509e0348fe17dd3248d35f4f2b52c/src/Finbuckle.MultiTenant.Core/Options/MultiTenantOptionsManager.cs

技巧是使用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>.

它可以按照示例https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeddd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L71所示进行连接

        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)

使用它https://github.com/Finbuckle/Finbuckle.MultiTenant/blob/3c94ab2848758de7c9d0154aeddd4820dd545fbf/src/Finbuckle.MultiTenant.Core/DependencyInjection/MultiTenantBuilder.cs#L43


 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现在,您可以注册与接受的答案相同的租户特定选项。