【问题标题】:Configuring asp.net core identity oauth2 authentication with user provided oauth2 credentials使用用户提供的 oauth2 凭据配置 asp.net 核心身份 oauth2 身份验证
【发布时间】:2020-07-31 10:27:40
【问题描述】:

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

我目前的问题是,凭据(oauth2 客户端 ID 和客户端密码,以及基本 URL)由组织管理员提供并存储在数据库中。我想为每个组织提供自己的子域,使用 Gitlab 登录按钮应将用户重定向到他们的 gitlab 实例并遵循通常的 oauth2 流程进行身份验证,但我不知道如何配置 asp.net 核心身份框架动态决定(基于子域)哪些凭据用于 oauth2 流。所有教程和微软提供的文档都假定您只提供了一个“硬编码”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);
            }
      };
    });
}

如何实施这样的系统?

【问题讨论】:

  • 我不了解 Gitlab,但 ASP.NET Core 允许您根据需要为任何使用选项模式的任何请求动态设置选项。如何配置 OAuth 权限、客户端 ID 和密码?
  • 目前我将它们设置在ConfigureServices方法的Startup类中,据我所知,该方法仅在启动时运行一次。
  • 你能告诉我ConfigureServices的相关代码吗?
  • 我添加了 ConfigureServices 方法的相关部分。请注意,此代码使用Configuration 的静态方法,而不是我使用更动态方法的失败实验。
  • 应该是可行的。请稍等,直到我找到我在我的一个项目中使用的示例,以便我为您写一个答案。

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


【解决方案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。我像这样使用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

【讨论】:

  • @TimKaechele 不客气,请查看我的编辑,因为我已修改 ConfigureServices 方法以阐明应如何注册依赖项。
猜你喜欢
  • 1970-01-01
  • 2019-01-24
  • 2014-01-23
  • 1970-01-01
  • 1970-01-01
  • 2021-06-11
  • 1970-01-01
  • 2018-02-28
  • 2017-11-28
相关资源
最近更新 更多