Настройка asp. net идентификации ядра oauth2 с предоставленными пользователем учетными данными oauth2 - PullRequest
0 голосов
/ 17 апреля 2020

Я создаю приложение, которое расширяет возможности пользователя gitlab, и администраторы организации (организации являются арендаторами в системе) могут настроить установку gitlab (зарегистрировать приложение OAuth2 в своем экземпляре gitlab), и обычные пользователи в организации могут просто аутентифицируйте себя с помощью своей учетной записи gitlab через OAuth2.

Моя проблема на данный момент заключается в том, что учетные данные (идентификатор клиента oauth2 и секрет клиента, а также базовый URL) предоставляются администратором организации и хранятся в базе данных. Я хочу предоставить каждой организации свой собственный поддомен, и кнопка «Войти с Gitlab» должна перенаправить пользователя на его экземпляр gitlab и следовать обычному потоку oauth2 для аутентификации, но я не могу понять, как настроить asp. net базовая структура идентификации, чтобы на лету принять решение (на основе поддоменов), какие учетные данные использовать для потока oauth2. Все учебные пособия и документы, предоставляемые Microsoft, предполагают, что у вас есть только один «жестко запрограммированный» oauth2 (обычно настраивается в методе ConfigureServices класса Startup).

Моя текущая реализация соответствует документации, предоставленной Microsoft, и выглядит следующим образом:

public void ConfigureServices(IServiceCollection services) 
{      
  // ... 
  services.AddAuthentication(options =>
  {
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = "Gitlab";
  }).AddCookie()
    .AddOAuth("Gitlab", options =>
    {
      options.ClientId = Configuration["Gitlab:ClientId"];
      options.ClientSecret = Configuration["Gitlab:ClientSecret"];
      options.CallbackPath = new Microsoft.AspNetCore.Http.PathString("/signin-gitlab");

      options.AuthorizationEndpoint = Configuration["Gitlab:BaseUrl"] + "/oauth/authorize";
      options.TokenEndpoint = Configuration["Gitlab:BaseUrl"] + "/oauth/token";

      options.UserInformationEndpoint = Configuration["Gitlab: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);
            }
      };
    });
}

Как я могу реализовать такую ​​систему?

1 Ответ

1 голос
/ 18 апреля 2020

Обработчик OAuth использует шаблон параметров для конфигурации, что означает, что вы можете использовать его для установки таких свойств, как ClientId, ClientSecret, et c, динамически, для каждого запроса на основе свойств запроса.

Вам нужно сделать следующее (пожалуйста, имейте в виду любые проблемы с компиляцией, я использовал его с разными вариантами, так что пишу это в основном из моей головы):

  1. Изменить тело 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;
}

И чтобы не забывать, я хочу приписать исходный ответ для этой идеи: { ссылка }

...