openid connect-идентификация клиента при входе в систему
У меня есть мультитенантное приложение (одна база данных), которое позволяет использовать одно и то же имя пользователя/адрес электронной почты для разных клиентов.
Во время входа в систему (неявный поток) как я могу идентифицировать арендатора? Я подумал о следующих возможностях:
-
В момент регистрации попросите у пользователя учетную запись
slug
(company / tenant slug) и во время входа в систему пользователь должен предоставитьslug
вместе сusername
иpassword
., но нет параметров в открытых идентификатор запроса для отправки слизняк.
-
Создайте приложение
OAuth
в момент регистрации и используйтеslug
Какclient_id
. Во время входа в систему передайтеslug
вclient_id
, который я буду использовать, чтобы получить идентификатор клиента и продолжить проверку пользователя.
Хорош ли такой подход?
Редактировать:
Также пытались сделать slug частью маршрута param
.EnableTokenEndpoint("/connect/{slug}/token");
, но openiddict не поддерживает это.
2 ответа:
Подход, предложенный McGuire, будет работать с OpenIddict (вы можете получить доступ к свойству
Вместо этого рассмотрите возможность запуска эмитента на одного арендатора. Для этого у вас есть по крайней мере 2 варианта:acr_values
черезOpenIdConnectRequest.AcrValues
) но это не рекомендуемый вариант (он не идеален с точки зрения безопасности: поскольку эмитент одинаков для всех арендаторов, они в конечном итоге используют одни и те же ключи подписи).
Дайте 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 и хэш и т. д.)