使用用户提供的oauth2凭据配置asp.net核心身份oauth2身份验证

时间:2020-04-17 20:03:43

标签: asp.net-core oauth-2.0 asp.net-core-identity

我构建了一个扩展gitlab用户体验的应用程序,组织的管理员(组织是系统中的租户)可以配置其gitlab安装(在其gitlab实例中注册OAuth2应用程序),而组织中的普通用户可以通过OAuth2使用其gitlab帐户进行身份验证。

目前,我的问题是,凭据(oauth2客户端ID和客户端密钥以及基本URL)由组织管理员提供,并存储在数据库中。我想给每个组织一个自己的子域,并且使用Gitlab登录按钮应该将用户重定向到他们的gitlab实例,并按照通常的oauth2流程进行身份验证,但是我不知道如何配置asp.net核心身份框架即时(基于子域)决定要用于oauth2流的凭据。所有教程和Microsoft提供的文档都假定您仅提供了一个“硬编码” oauth2(通常在Startup类的ConfigureServices方法中进行配置)。

我当前的实现遵循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 个答案:

答案 0 :(得分:1)

OAuth处理程序使用选项模式进行配置,这意味着您可以利用它来根据请求属性动态地基于每个请求设置属性ClientIdClientSecret

您需要执行以下操作(请耐心等待任何编译问题,我将其用于其他选项,因此主要是从我的脑海中写出来的):

  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>();
}
  1. 根据传入的请求实施租户解决逻辑,并将其注册到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
    }
}
  1. 使用高速缓存存储OAuthOptions的实例,并将其注册为DI并以单例形式注册到DI。我这样使用ConcurrentDictionary
public class OAuthOptionsCacheAccessor // register to DI as singleton
{
    public ConcurrentDictionary<(string name, string tenant), Lazy<OAuthOptions>> Cache =>
        new ConcurrentDictionary<(string, string), Lazy<OAuthOptions>>();
}
  1. 实现选项初始化程序,该初始化程序将基于解析的租户返回正确的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.");
}
  1. 最后,实现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;
}

不要忘记,我想将这个想法的原始答案归因于https://stackoverflow.com/a/52977687/828023