Несколько уполномоченных / издателей JWT в Asp.Net Core

Я пытаюсь получить аутентификацию канала-носителя JWT в шлюзе ASP.Net API, используя Ocelot для работы с несколькими органами / издателями. Один из эмитентов - Auth0, а другой - внутренний сервер аутентификации на основе IdentityServer4; мы пытаемся перейти от Auth0, но у нас есть внешние клиенты, которые все еще зависят от него, поэтому мы хотели бы поддерживать оба, пока все не будет полностью протестировано для их переключения.

Согласно этому сообщению в блоге MSDN, можно использовать несколько прав доступа, установив TokenValidationParameters.ValidIssuers вместо JwtBearerOptions.Authority, Тем не менее, я проверил это с Ocelot и без него, и никакая аутентификация не происходит, если Орган не настроен на орган, выдавший токен, независимо от содержимого TokenValidationParameters.ValidIssuers,

Кто-нибудь знает, как заставить это работать? Вот как я настраиваю аутентификацию. Это работает, только если закомментированная строка не закомментирована (и только для токенов, выпущенных этим единственным органом). Я ожидаю, что Ocelot или ASP.Net Core получат ключ от сервера выдачи; оба обеспечивают JWK с помощью.well-known/openid-configuration, которая работает с промежуточным программным обеспечением 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()
                };
            });
    }

Вывод Ocelot, когда клиент имеет неверного эмитента (или когда мы используем TokenValidationParameters.ValidIssuer/ValidIssuers) подключается это:

[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". Как видите, Оцелот не говорит, в чем именно заключается проблема. Ядро промежуточного программного обеспечения JWT ASP.Net Core (без Ocelot) просто говорит, что подпись недействительна. Я подозреваю, что это либо не смотрит на TokenValidationParametersили я не понял их цели.

3 ответа

Решение

Я понял, как это сделать:

  1. Создайте конструктор аутентификации с services.AddAuthentication(), Вы можете установить схему по умолчанию (на "Носитель"), если хотите, но это не обязательно.

  2. Добавьте столько разных конфигураций JWT Bearer, сколько хотите authenticationBuilder.AddJwtBearer()каждый со своим собственным ключом (например, "Auth0", "IS4", ...). Я использовал цикл над массивом в appsettings.json

  3. Создайте схему политики с authenticationBuilder.AddPolicyScheme и дайте ему название схемы "Носитель" (используйте 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 поддерживал это, просто используйте "Носитель" в качестве ключа провайдера аутентификации, и политика выбора схемы будет автоматически активирована.

Это рабочий пример:

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

Рабочее решение для .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
                 };
             });
    
         }
    
Другие вопросы по тегам