Asp.Net Core 中的多个 JWT 授权机构/发行人

Posted

技术标签:

【中文标题】Asp.Net Core 中的多个 JWT 授权机构/发行人【英文标题】:Multiple JWT authorities/issuers in Asp.Net Core 【发布时间】:2019-03-27 21:37:48 【问题描述】:

我正在尝试使用 Ocelot 在 ASP.Net API 网关中获取 JWT 不记名身份验证,以与多个授权/颁发者合作。一个发行者是Auth0,另一个是基于IdentityServer4的内部认证服务器;我们正在尝试从 Auth0 迁移,但仍有外部客户端依赖它,因此我们希望同时支持这两种客户端,直到一切都经过全面测试以供切换。

根据this MSDN blog post,应该可以通过设置TokenValidationParameters.ValidIssuers而不是JwtBearerOptions.Authority来使用多个权限。但是,无论TokenValidationParameters.ValidIssuers 的内容如何,​​我都在使用和不使用Ocelot 的情况下对此进行了测试,如果没有将授权设置为颁发令牌的授权,则不会发生身份验证。

有人知道如何让这个工作吗?这就是我设置身份验证的方式。它仅在注释行未注释时才有效(并且仅适用于由该单一机构颁发的令牌)。我期待 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】:

.net 5 的有效解决方案。

这适用于多个 JWT 不记名令牌发行者

默认架构会路由到相应的架构

// Get list of domains and audience from the config
     var authorities = Configuration["Auth:Domain"].Split(',').Distinct().ToList();
     var audience = Configuration["Auth:Audience"];
     // Add default empty schema schema selection policy
     var authenticationBuilder = services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(
         options =>
         
             // forward to corresponding schema based on token's issuer 
             // this will read the token and check the token issues , if the token issuer is registered in config then redirect to that schema
             options.ForwardDefaultSelector = context =>
             
                 string authorization = context.Request.Headers[HeaderNames.Authorization];

                 if (!string.IsNullOrEmpty(authorization))
                 
                     if (authorization.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
                     
                         var token = authorization.Substring("Bearer ".Length).Trim();

                         var jwtHandler = new JwtSecurityTokenHandler();
                         if (jwtHandler.CanReadToken(token))
                         
                             var jwtToken = jwtHandler.ReadJwtToken(token);
                             if (authorities.Contains(jwtToken.Issuer))
                                 return jwtToken.Issuer;
                         
                     
                 
                 return null;
             ;
         );

     // Register all configured schemas 
     foreach (var auth in authorities)
     
         authenticationBuilder.AddJwtBearer(auth, options =>
         

             options.SaveToken = true;
             options.Audience = audience;
             options.Authority = auth;
             options.TokenValidationParameters = new TokenValidationParameters
             
                 NameClaimType = "sub",
                 ValidateIssuer = true,
                 ValidateAudience = true,
                 ValidateLifetime = true,
                 RequireSignedTokens = true,
                 ValidateIssuerSigningKey = true
             ;
         );

     

【讨论】:

这个!当我遇到 JwtBearer 配置错误之一的问题时帮助了我 - 它阻止了流程继续流向其他人,无法处理 JWT 令牌。假设我配置了 A、B 和 C。 A错了。 C 的令牌首先去了 A,失败了,从未尝试过 B,从未尝试过 C,所以它抛出了 Http500,仅此而已。使用这个默认选择器,我能够定位特定的配置并绕过错误。谢谢!【参考方案2】:

这是工作示例:

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
     );

【讨论】:

谢谢!!这比其他答案容易得多。我只用了大约 5 分钟就实现了这个解决方案。【参考方案3】:

我想出了怎么做:

    使用services.AddAuthentication() 创建一个身份验证构建器。您可以根据需要设置默认方案(为“Bearer”),但这不是必需的。

    使用 authenticationBuilder.AddJwtBearer() 添加任意数量的不同 JWT 承载配置,每个配置都有自己的密钥(例如“Auth0”、“IS4”……)。我在 appsettings.json 中的数组上使用了循环

    使用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”作为身份验证提供者密钥,就会自动调用方案选择器策略。

【讨论】:

为什么还要检查“WWWAuthenticate”标头?这不是响应头吗,通常不包含在请求中? 一个绝妙的答案。谢谢!

以上是关于Asp.Net Core 中的多个 JWT 授权机构/发行人的主要内容,如果未能解决你的问题,请参考以下文章

ASP.NET Core 2 授权属性 jwt

Jwt 和 ASP.NET CORE 授权 AspNetRoleClaims

asp.net core 2.0 授权在身份验证(JWT)之前触发

为啥我在 Asp.Net CORE 中使用 JWT 获得 401 未经授权?

ASP.NET Core WebApi Jwt 基于角色的授权不起作用

ASP.NET Core SPA 中基于 JWT 的身份验证 - 前端验证