Обработчик OAuth использует шаблон параметров для конфигурации, что означает, что вы можете использовать его для установки таких свойств, как ClientId
, ClientSecret
, et c, динамически, для каждого запроса на основе свойств запроса.
Вам нужно сделать следующее (пожалуйста, имейте в виду любые проблемы с компиляцией, я использовал его с разными вариантами, так что пишу это в основном из моей головы):
- Изменить тело
ConfigureServices
следующим образом:
public void ConfigureServices(IServiceCollection services)
{
// ...
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = "Gitlab";
}).AddCookie()
.AddOAuth("Gitlab", delegate { }); // Don't specify hard coded OAuth options. Instead, you will return them from an options provider.
services.AddTransient<TenantResolver>();
services.AddSingleton<OAuthOptionsCacheAccessor>();
services.AddTransient<IConfigureNamedOptions<OAuthOptions>, OAuthOptionsInitializer>();
services.AddTransient<IOptionsMonitor<OAuthOptions>, OAuthOptionsProvider>();
}
Внедрите свою логику разрешения арендатора c на основе входящего запроса и зарегистрируйте ее в DI. Например:
public class TenantResolver // don't forget to register this to DI in ConfigureServices
{
private readonly IHttpContextAccessor httpContextAccessor;
public TenantAuthorityResolver(IHttpContextAccessor httpContextAccessor)
{
this.httpContextAccessor = httpContextAccessor;
}
public string GetCurrentTenant()
{
// TODO: Read the current request from httpContextAccessor.HttpContext.Request
// and parse it to resolve the current tenant Id based on your own logic
}
}
Используйте кэш для хранения ваших экземпляров
OAuthOptions
и зарегистрируйте его в DI как singleton. Я использовал
ConcurrentDictionary
так:
public class OAuthOptionsCacheAccessor // register to DI as singleton
{
public ConcurrentDictionary<(string name, string tenant), Lazy<OAuthOptions>> Cache =>
new ConcurrentDictionary<(string, string), Lazy<OAuthOptions>>();
}
Реализуйте инициализатор параметров, который будет возвращать правильный экземпляр
OAuthOptions
на основе разрешенного арендатора, и зарегистрируйте этот класс в DI в качестве временной зависимости.
public class OAuthOptionsInitializer : IConfigureNamedOptions<OAuthOptions> // register as transient
{
private readonly IDataProtectionProvider dataProtectionProvider;
private readonly TenantResolver tenantResolver;
public OAuthOptionsInitializer(
IDataProtectionProvider dataProtectionProvider,
TenantResolver tenantResolver)
{
this.dataProtectionProvider = dataProtectionProvider;
this.tenantResolver = tenantResolver;
}
public void Configure(string name, OAuthOptions options)
{
if (!string.Equals(name, OpenIdConnectDefaults.AuthenticationScheme, StringComparison.Ordinal))
{
return;
}
var tenant = tenantResolver.GetCurrentTenant();
// TODO: You will probably want to save your per-tenant OAuth options
// in the database or somewhere, so now is the time to obtain those.
// I also recommend using Nito.AsyncEx to be able to safely call async methods from here
var savedOptions = Nito.AsyncEx.AsyncContext.Run(async () => await GetSavedOptions(tenant));
options.ClientId = savedOptions.ClientId;
options.ClientSecret = savedOptions.ClientSecret;
options.CallbackPath = new Microsoft.AspNetCore.Http.PathString("/signin-gitlab");
options.AuthorizationEndpoint = savedOptions.BaseUrl + "/oauth/authorize";
options.TokenEndpoint = savedOptions.BaseUrl + "/oauth/token";
options.UserInformationEndpoint = savedOptions.BaseUrl + "/api/v4/user";
options.ClaimActions.MapJsonKey(ClaimTypes.NameIdentifier, "id");
options.ClaimActions.MapJsonKey(ClaimTypes.Name, "name");
options.ClaimActions.MapJsonKey("gitlab:avatar_url", "avatar_url");
options.ClaimActions.MapJsonKey("gitlab:profile_url", "web_url");
options.SaveTokens = true;
options.Events = new OAuthEvents
{
OnCreatingTicket = async context =>
{
var request = new HttpRequestMessage(HttpMethod.Get, context.Options.UserInformationEndpoint);
request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", context.AccessToken);
var response = await context.Backchannel.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, context.HttpContext.RequestAborted);
response.EnsureSuccessStatusCode();
var user = JsonSerializer.Deserialize<JsonElement>(await response.Content.ReadAsStringAsync());
context.RunClaimActions(user);
}
};
}
public void Configure(OpenIdConnectOptions options)
=> Debug.Fail("This infrastructure method shouldn't be called.");
}
И, наконец, реализуйте поставщик опций OAuth и зарегистрируйте его в DI как переходный процесс:
public class OAuthOptionsProvider : IOptionsMonitor<OAuthOptions>
{
private readonly OAuthOptionsCacheAccessor cacheAccessor;
private readonly IOptionsFactory<OAuthOptions> optionsFactory;
private readonly TenantResolver tenantResolver;
public OAuthOptionsProvider(
IOptionsFactory<OAuthOptions> optionsFactory,
TenantResolver tenantResolver,
OAuthOptionsCacheAccessor cacheAccessor)
{
this.cacheAccessor = cacheAccessor;
this.optionsFactory = optionsFactory;
this.tenantAuthorityResolver = tenantAuthorityResolver;
}
public OAuthOptions CurrentValue => Get(Options.DefaultName);
public OAuthOptions Get(string name)
{
var tenant = tenantResolver.GetCurrentTenant();
Lazy<OAuthOptions> Create() => new Lazy<OAuthOptions>(() => optionsFactory.Create(name));
return cacheAccessor.Cache.GetOrAdd((name, tenant), _ => Create()).Value;
}
public IDisposable OnChange(Action<OAuthOptions, string> listener) => null;
}
И чтобы не забывать, я хочу приписать исходный ответ для этой идеи: { ссылка }