異なるテナント間で同じユーザー名/電子メールを許可するマルチテナント(単一データベース)アプリケーションがあります。
ログイン時(暗黙のフロー)、テナントを特定するにはどうすればよいですか?私は次の可能性を考えました:
登録時にユーザーにアカウントslug
(会社/テナントスラッグ)を要求し、ログイン時にユーザーはslug
とusername
とともにpassword
を提供する必要があります。
ただし、open idリクエストには、スラッグを送信するためのパラメーターはありません。
登録時にOAuth
アプリケーションを作成し、slug
をclient_id
として使用します。ログイン時に、client_id
にslug
を渡します。これを使用して、テナントIDを取得し、さらにユーザーの検証に進みます。
このアプローチは問題ありませんか?
編集:
スラッグをルートパラメータの一部にしようとしました
.EnableTokenEndpoint("/connect/{slug}/token");
しかし、openiddictはそれをサポートしていません。
McGuireによって提案されたアプローチはOpenIddictで機能します(acr_values
を介してOpenIdConnectRequest.AcrValues
プロパティにアクセスできます)が、推奨されるオプションではありません(セキュリティの観点からは理想的ではありません。発行者はすべてのテナントで同じであるため、最終的に同じ署名キーを共有することになります)。
代わりに、テナントごとに発行者を実行することを検討してください。そのためには、少なくとも2つのオプションがあります。
Give OrchardCoreのOpenIDモジュール 試してみてください:OpenIddictに基づいており、マルチテナンシーをネイティブにサポートしています。まだベータ版ですが、積極的に開発されています。
OpenIddictが使用するオプションモニターをオーバーライドして、テナントごとのオプションを使用します。
カスタムモニターとパスベースのテナント解決を使用した、2番目のオプションの簡略化された例を次に示します。
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.
}
}
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応答を取得する必要があります)。
OAuthプロセスで正しい方向に進んでいます。クライアントWebアプリのスタートアップコードにOpenIDConnectスキームを登録するときに、OnRedirectToIdentityProvider
イベントのハンドラーを追加して使用します。 「slug」値を「tenant」ACR値として追加します(OIDCが "Authentication Context Class Reference" と呼ぶもの)。
これをサーバーに渡す方法の例を次に示します。
.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}";
}
};
}
これがどの種類のサーバーに行くかを指定しませんでしたが、ACR(および「テナント」値)はOIDCの標準部分です。 Identity Server 4を使用している場合は、ログインを処理するクラスに Interaction Service を挿入し、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.
}
}
}
個々のユーザーアカウントを識別するという点では、一意のユーザーIDとして「サブジェクトID」を使用するというOIDC標準に固執すると、作業がはるかに簡単になります。 (つまり、テナントの「スラッグ」、ユーザーの電子メールアドレス、パスワードのソルトとハッシュなどのユーザーデータを格納するキーを作成します。)