Asp.Net Core中多个JWT授权/发行者

20

我正在尝试使用Ocelot在ASP.Net API网关中实现JWT承载身份验证,以便与多个授权机构/颁发者一起使用。一个颁发者是Auth0,另一个是基于IdentityServer4的内部身份验证服务器;我们试图摆脱Auth0,但仍有外部客户端依赖它,因此我们希望支持两个,直到一切都为他们完全测试并且可以切换。

根据这篇 MSDN 博客文章,可以通过设置TokenValidationParameters.ValidIssuers而不是JwtBearerOptions.Authority来使用多个授权机构。然而,我已经使用和没有使用Ocelot进行了测试,如果Authority没有设置为颁发令牌的颁发机构,就不会进行任何身份验证,无论TokenValidationParameters.ValidIssuers的内容如何。

有人知道如何让它工作吗?这是我的身份验证设置方式。只有取消注释的那一行才起作用(并且仅适用于由该单个机构颁发的令牌)。我期望Ocelot或ASP.Net Core从颁发服务器获取密钥;两者都通过.well-known/openid-configuration提供JWKs,这对于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 的输出如下:

Translated Content:

当客户端使用错误的颁发者或者我们使用 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身份验证,因此在“通过”之后没有用户名。正如您所看到的,Ocelot没有说明确切的问题所在。ASP.Net Core JWT承载中间件(不带Ocelot)只说签名无效。我怀疑它要么没有查看TokenValidationParameters,要么我误解了它们的目的。
3个回答

24

我弄清楚了如何做:

  1. 使用 services.AddAuthentication() 创建一个身份验证构建器。如果需要,您可以设置默认方案(默认为“Bearer”)。

  2. 使用 authenticationBuilder.AddJwtBearer() 添加多个不同的JWT承载配置,每个配置都有自己的密钥(例如“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"作为认证提供程序键,选择方案策略将自动调用。


你为什么要检查“WWWAuthenticate”头呢?这不是一个请求头,通常不会包含在请求中吗? - Pieter Sap
一个很棒的答案。谢谢! - mitchsnitchel
很棒的答案!当没有头信息时,我不是返回“null”,而是返回其中一种身份验证方案并让它尝试(失败)进行身份验证。这将导致用户未经身份验证。 - Isak Savo
此外,微软在此方案上还有更多的文档,请参见:https://learn.microsoft.com/en-us/aspnet/core/security/authorization/limitingidentitybyscheme?view=aspnetcore-7.0 - Isak Savo

15

这是一个工作示例:

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分钟就实现了这个解决方案。 - guyfromfargo

7

一个适用于 .NET 5 的可行解决方案。

  • This will work for multiple JWT bearer token issuers

  • Default schema will do the routing to the corresponding schema

    // 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,并且就此结束了。使用此默认选择器,我能够针对特定的配置进行处理,从而避免了错误发生。谢谢! - podvlada
这对我很有帮助。真的非常感谢您发布这个! - G3tinmybelly
这正是帮助我解决问题的方法。非常感谢!巨大的感谢,兄弟!保重! - Farkhod
这绝对是这里最好的答案 - Cobus Kruger
这绝对是这里最好的答案 - undefined

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接