Несколько уполномоченных / издателей 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 ответа
Я понял, как это сделать:
Создайте конструктор аутентификации с
services.AddAuthentication()
, Вы можете установить схему по умолчанию (на "Носитель"), если хотите, но это не обязательно.Добавьте столько разных конфигураций JWT Bearer, сколько хотите
authenticationBuilder.AddJwtBearer()
каждый со своим собственным ключом (например, "Auth0", "IS4", ...). Я использовал цикл над массивом в appsettings.jsonСоздайте схему политики с
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 }; }); }