openid connect-идентификация клиента при входе в систему


У меня есть мультитенантное приложение (одна база данных), которое позволяет использовать одно и то же имя пользователя/адрес электронной почты для разных клиентов.

Во время входа в систему (неявный поток) как я могу идентифицировать арендатора? Я подумал о следующих возможностях:

  1. В момент регистрации попросите у пользователя учетную запись slug (company / tenant slug) и во время входа в систему пользователь должен предоставить slug вместе с username и password.

    , но нет параметров в открытых идентификатор запроса для отправки слизняк.

  2. Создайте приложение OAuth в момент регистрации и используйте slug Как client_id. Во время входа в систему передайте slug в client_id, который я буду использовать, чтобы получить идентификатор клиента и продолжить проверку пользователя.

Хорош ли такой подход?

Редактировать:

Также пытались сделать slug частью маршрута param

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

, но openiddict не поддерживает это.

2 4

2 ответа:

Подход, предложенный McGuire, будет работать с OpenIddict (вы можете получить доступ к свойству acr_values через OpenIdConnectRequest.AcrValues) но это не рекомендуемый вариант (он не идеален с точки зрения безопасности: поскольку эмитент одинаков для всех арендаторов, они в конечном итоге используют одни и те же ключи подписи).

Вместо этого рассмотрите возможность запуска эмитента на одного арендатора. Для этого у вас есть по крайней мере 2 варианта:
  • Дайте OpenID модулю OrchardCore попробовать : он основан на 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;
    }
}
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: [порт] / fabrikam/.OpenID/OpenID-configuration (вы должны получить ответ JSON с метаданными OpenID Connect).

Вы на правильном пути с процессом OAuth. При регистрации схемы OpenID Connect в коде запуска клиентского веб-приложения добавьте обработчик для события OnRedirectToIdentityProvider и используйте его для добавления значения "slug" в качестве значения ACR "tenant" (что-то 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}";
        }
    };
}

Вы не указали, какой сервер это будет, но ACR (и значение" tenant") являются стандартные части 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.
        }
    }
}

С точки зрения идентификации индивидуальных учетных записей пользователей, ваша жизнь будет намного проще, если вы будете придерживаться стандарта OIDC использования "subject ID" в качестве уникальный идентификатор пользователя. (Другими словами, сделайте это ключом, в котором вы храните свои пользовательские данные, такие как клиент "slug", адрес электронной почты пользователя, пароль salt и хэш и т. д.)