web-dev-qa-db-ja.com

openidconnect-ログイン中にテナントを識別する

異なるテナント間で同じユーザー名/電子メールを許可するマルチテナント(単一データベース)アプリケーションがあります。

ログイン時(暗黙のフロー)、テナントを特定するにはどうすればよいですか?私は次の可能性を考えました:

  1. 登録時にユーザーにアカウントslug(会社/テナントスラッグ)を要求し、ログイン時にユーザーはslugusernameとともにpasswordを提供する必要があります。

    ただし、open idリクエストには、スラッグを送信するためのパラメーターはありません。

  2. 登録時にOAuthアプリケーションを作成し、slugclient_idとして使用します。ログイン時に、client_idslugを渡します。これを使用して、テナントIDを取得し、さらにユーザーの検証に進みます。

このアプローチは問題ありませんか?

編集:

スラッグをルートパラメータの一部にしようとしました

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

しかし、openiddictはそれをサポートしていません。

7
adnan kamili

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

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応答を取得する必要があります)。

14
Kévin Chalet

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標準に固執すると、作業がはるかに簡単になります。 (つまり、テナントの「スラッグ」、ユーザーの電子メールアドレス、パスワードのソルトとハッシュなどのユーザーデータを格納するキーを作成します。)

2
McGuireV10