Asp.net-Core

Asp.Net Core 中的多個 JWT 授權機構/發行人

  • April 30, 2021

我正在嘗試使用 Ocelot 在 ASP.Net API 網關中獲得 JWT 不記名身份驗證,以與多個授權機構/頒發者合作。一個發行者是Auth0,另一個是基於IdentityServer4的內部認證伺服器;我們正在嘗試從 Auth0 遷移,但仍有外部客戶端依賴它,因此我們希望同時支持這兩種客戶端,直到一切都經過全面測試以供切換。

根據this MSDN blog post,應該可以通過設置TokenValidationParameters.ValidIssuers而不是使用多個權限JwtBearerOptions.Authority。但是,我已經在使用和不使用 Ocelot 的情況下對此進行了測試,如果沒有將授權設置為頒發令牌的授權,則不會發生身份驗證,無論TokenValidationParameters.ValidIssuers.

有誰知道如何讓這個工作?這就是我設置身份驗證的方式。它僅在註釋行未註釋時才有效(並且僅適用於由該單一機構頒發的令牌)。我期待 Ocelot 或 ASP.Net Core 從發布伺服器獲取密鑰;兩者都通過 .well-known/openid-configuration 提供 JWK,它與 ASP.Net Core 中間件一起使用。

public static void AddJwtBearerAuthentication(this IServiceCollection services, IConfiguration configuration)
{
   services
       .AddAuthentication(options =>
       {
           options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
           options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
       })
       .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
       {
           //options.Authority = configuration["Jwt:Authority"];
           options.Audience  = configuration["Jwt:Audience"];
           options.TokenValidationParameters = new TokenValidationParameters
           {
               ValidateIssuer           = true,
               ValidateIssuerSigningKey = true,
               ValidateAudience         = true,
               ValidAudience            = configuration["Jwt:Audience"],
               ValidIssuers             = configuration
                   .GetSection("Jwt:Authorities")
                   .AsEnumerable()
                   .Select(kv => kv.Value)
                   .Where(s => !string.IsNullOrEmpty(s))
                   .ToArray()
           };
       });
}

當有錯誤發行者的客戶端(或當我們使用TokenValidationParameters.ValidIssuer/時ValidIssuers)連接時,Ocelot 的輸出是:

[16:35:37 WRN] requestId: _____, previousRequestId: no previous request id, message: Error Code: UnauthenticatedError Message: Request for authenticated route _____ by  was unauthenticated errors found in ResponderMiddleware. Setting error response for request path:_____, request method: POST

這是一個 client_credentials 身份驗證,因此在“by”之後缺少使用者名。正如您所看到的,Ocelot 並沒有說明確切的問題是什麼。ASP.Net Core JWT 承載中間件(沒有 Ocelot)只是說簽名無效。我懷疑它要麼沒有看TokenValidationParameters,要麼我誤解了它們的目的。

我想出瞭如何做到這一點:

  1. 使用services.AddAuthentication(). 如果需要,您可以設置預設方案(為“Bearer”),但這不是必需的。
  2. 添加任意數量的不同 JWT Bearer 配置authenticationBuilder.AddJwtBearer(),每個配置都有自己的密鑰(例如“Auth0”、“IS4”、…)。我在 appsettings.json 中的數組上使用了循環
  3. 創建一個策略方案authenticationBuilder.AddPolicyScheme並為其指定方案名稱“Bearer”(用於JwtBearerDefaults.AuthenticationScheme避免在程式碼中包含魔術字元串)並options.ForwardDefaultSelector在回調中設置一個返回其他方案名稱之一的函式(“Auth0”、“IS4”或不管你放什麼)取決於一些標準。在我的情況下,它只是在 JWT 頒發者中查找方案名稱(如果頒發者包含“auth0”,則使用 Auth0 方案)。

程式碼:

public static void AddMultiSchemeJwtBearerAuthentication(
   this IServiceCollection services,
   IConfiguration configuration
)
{
   // Create JWT Bearer schemes.
   var schemes = configuration
       .GetSection("Jwt")
       .GetChildren()
       .Select(s => s.Key)
       .ToList()
   ;
   var authenticationBuilder = services.AddAuthentication();
   foreach (var scheme in schemes)
   {
       authenticationBuilder.AddJwtBearer(scheme, options =>
       {
           options.Audience  = configuration[$"Jwt:{scheme}:Audience"];
           options.Authority = configuration[$"Jwt:{scheme}:Authority"];
       });
   }

   // Add scheme selector.
   authenticationBuilder.AddPolicyScheme(
       JwtBearerDefaults.AuthenticationScheme,
       "Selector",
       options =>
       {
           options.ForwardDefaultSelector = context =>
           {
               // Find the first authentication header with a JWT Bearer token whose issuer
               // contains one of the scheme names and return the found scheme name.
               var authHeaderNames = new[] {
                   HeaderNames.Authorization,
                   HeaderNames.WWWAuthenticate
               };
               StringValues headers;
               foreach (var headerName in authHeaderNames)
               {
                   if (context.Request.Headers.TryGetValue(headerName, out headers) && !StringValues.IsNullOrEmpty(headers))
                   {
                       break;
                   }
               }

               if (StringValues.IsNullOrEmpty(headers))
               {
                   // Handle error. You can set context.Response.StatusCode and write a
                   // response body. Returning null invokes default scheme which will raise
                   // an exception; not sure how to fix this so the request is rejected.
                   return null;
               }

               foreach (var header in headers)
               {
                   var encodedToken = header.Substring(JwtBearerDefaults.AuthenticationScheme.Length + 1);
                   var jwtHandler = new JwtSecurityTokenHandler();
                   var decodedToken = jwtHandler.ReadJwtToken(encodedToken);
                   var issuer = decodedToken?.Issuer?.ToLower();
                   foreach (var scheme in schemes)
                   {
                       if (issuer?.Contains(scheme.ToLower()) == true)
                       {
                           // Found the scheme.
                           return scheme;
                       }
                   }
               }
               // Handle error.
               return null;
           };
       }
   );
}

讓 Ocelot 支持這一點不需要什麼特別的,只需使用“Bearer”作為身份驗證提供程序密鑰,就會自動呼叫方案選擇器策略。

這是工作範例:

public void ConfigureServices(IServiceCollection services)
{
  services.AddAuthentication(options => 
  {
      options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
  })
   //set default authentication 
   .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
   {
       //set the next authentication configuration to be used
       options.ForwardDefaultSelector = ctx => "idp4";

       //...rest of the options goes here
       };
   })
   .AddJwtBearer("idp4", options => 
    {
       //set the next authentication configuration to be used
       options.ForwardDefaultSelector = ctx => "okta";
       //options goes here
    })
   .AddJwtBearer("okta", options => 
    {
       //options goes here
    });

引用自:https://stackoverflow.com/questions/52952600