Asp.net-Core

openid connect - 在登錄期間辨識租戶

  • May 11, 2020

我有一個多租戶(單數據庫)應用程序,它允許跨不同租戶使用相同的使用者名/電子郵件。

登錄時(隱式流)如何辨識租戶?我想到了以下幾種可能:

  1. 在註冊時向使用者詢問帳戶slug(公司/租戶 slug),在登錄時使用者應slug提供usernamepassword

但是open id請求中沒有參數來發送slug。 2. 在註冊時創建一個OAuth應用程序並slug用作client_id. 在登錄slugclient_id,我將使用它來獲取租戶 ID 並進一步驗證使用者。

這種方法好嗎?

編輯:

還嘗試使 slug 成為路線參數的一部分

.EnableTokenEndpoint("/connect/{slug}/token");

但 openiddict 不支持。

McGuire 建議的方法將適用於 OpenIddict(您可以通過 訪問該acr_values屬性OpenIdConnectRequest.AcrValues,但這不是推薦的選項(從安全形度來看,這並不理想:由於所有租戶的發行人都是相同的,他們最終共享相同的簽名鍵)。

相反,請考慮為每個租戶執行一個發行者。為此,您至少有 2 個選項:

  • 試試OrchardCore的 OpenID 模組:它基於 OpenIddict 並且原生支持多租戶。它仍處於測試階段,但正在積極開發中。
  • 覆蓋 OpenIddict 使用的選項監視器以使用 per-tenant options

這是第二個選項的簡化範例,使用自定義監視器和基於路徑的租戶解析:

實現您的租戶解析邏輯。例如:

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

實現自定義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.
   }
}

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

要確認這是否正常工作,請導航到http://localhost:$$ port $$/fabrikam/.well-known/openid-configuration(你應該得到一個帶有 OpenID Connect 元數據的 JSON 響應)。

引用自:https://stackoverflow.com/questions/49596938