Комментарии 11
Мне кажется, ваше решение сильно переусложнено. И есть несколько ошибок.
Давайте сначала разберём ApiKeyAuthenticationHandler
из первой части:
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!TryGetApiKey(out var apiKey, out var failureMessage))
{
return AuthenticateResult.Fail(failureMessage!);
}
Когда схеме аутентификации недостаточно данных (например, заголовок с API-ключом отсутствует вообще), она должна возвращать AuthenticationResult.NoResult()
. Это нужно чтобы не заваливать аутентификацию целиком, а дать другим схемам возможность тоже отработать. О, это же как раз наш случай!
AuthenticationResult.Fail(...)
- это для случаев, когда схема уже однозначно определена, например, если в запросе передан нужный заголовок, но в нём нет ключа или он невалидный:
ApiKeyAuthenticationHandler.cs
protected override Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!Context.Request.Headers.TryGetValue("X-API-Key", out var apiKeyHeaderValue))
{
return Task.FromResult(AuthenticateResult.NoResult());
}
// Проверяем ключ...
if (apiKeyHeaderValue != "14f259ea-1b55-4135-a335-b4d3d6a1030f")
{
return Task.FromResult(AuthenticateResult.Fail("Invalid API key"));
}
var principal = new ClaimsPrincipal(new ClaimsIdentity([/* claims go here... */], "ApiKey"));
return Task.FromResult(AuthenticateResult.Success(new(principal, "ApiKey")));
}
Теперь посмотрим на CompositeAuthenticationHandler
. Он не нужен :) Нужно настроить политику авторизации:
builder.Services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder("ApiKey", "Bearer")
.RequireAuthenticatedUser()
.Build();
});
Тогда сначала произойдёт попытка аутентифицироваться по ключу, а если не получилось, то по токену. Конечно же, атрибут [Authorize(AuthenticationSchemes = ...)]
с указанием конкретных схем продолжит работать.
Да вы правы, проверил, не поспотрел в сторону AuthorizationPolicyBuilder
сразу потому что у меня Composite немного сложнее чем в статье, я добавляю WWW-Authenticate в ответ для каждой поддерживаемой схемы чтобы информировать пользователя о необходиости использовать ApiKey. Спасибо, статью уберу в архив, она тогда получается имеет мало смысла)
Еще раз перепроверил:
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using Test.Api.Auth;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
var configuration = builder.Configuration;
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, _ => { })
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
var authority = configuration["Jwt:Authority"];
if (string.IsNullOrWhiteSpace(authority))
throw new Exception("Unable to load Jwt:Authority configuration.");
var metadataAddress = configuration["Jwt:MetadataAddress"];
if (string.IsNullOrWhiteSpace(metadataAddress))
throw new Exception("Unable to load Jwt:MetadataAddress configuration.");
options.Authority = authority;
options.MetadataAddress = metadataAddress;
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
});
builder.Services.AddAuthorizationBuilder()
.SetDefaultPolicy(new AuthorizationPolicyBuilder(ApiKeyAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
builder.Services.AddScoped<IApiKeyValidator, ApiKeyValidator>();
var app = builder.Build();
// Configure the HTTP request pipeline.
app.UseHttpsRedirection();
app.MapGet("/api/secure", [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] () =>
{
return new { Message = "This is a secure endpoint." };
}).RequireAuthorization();
app.Run();
И результат такой что я могу использовать ApiKey, т.е. Authorize атрибут становится бесполезен так как пропускает обе схемы все равно.

The AuthorizationPolicyBuilder
is used to build a policy that requires authentication using one of the provided authentication schemes.
И на всякий случай советую проверить везде где вы это используете, так как, это может непреднамеренно привести к несанкционированному доступу, если API должна поддерживать только один метод аутентификации. Например, если конечная точка предназначена для аутентификации с помощью JWT, но по умолчанию также разрешает аутентификацию с использованием API-ключа, это может подвергнуть endpoint рискам безопасности, при которых API-ключ может обойти более строгие проверки аутентификации.
Хм, да, вы правы. Если в [Authorize]
не указывать Policy
, берётся политика по умолчанию, а мы в ней уже разрешили оба метода аутентификации.
Но тогда получается всё ещё проще: политику авторизации по умолчанию мы не трогаем (или по крайней мере не указываем методы аутентификации):
builder.Services.AddAuthorization();
и перечисляем требуемые методы в [Authorize]
:
// Метод аутентификации по умолчанию
[Authorize]
// Только ApiKey
[Authorize(AuthenticationSchemes = "ApiKey")]
// ApiKey или Bearer
[Authorize(AuthenticationSchemes = "ApiKey,Bearer")]
Т.е. там где указано как вы пишите
// Только ApiKey[Authorize(AuthenticationSchemes = "ApiKey")]
Но если политика по умолчанию Jwt, то все равно Jwt будет работать, что противоречит ожидаемому поведению, не так ли?
Я пишу про проверки методов аутентификации а вы пытаетесь решить это с помощью политик авторизации.
Верно - потому что пропускать или запрещать запросы - это ответственность авторизации, отдельного шага после аутентификации.
Грубо говоря, аутентификация проверяет у пользователя документы, это может быть паспорт или водительские права, а авторизация решает, какие именно документы (уже проверенные к этому моменту) принимать - любые или, например, только паспорт.
Поддержу, это именно функция авторизации.
Если у нас на одном методе одна аутентификация, а на другом другая, это разные политики. В случае, когда "можно обойти" защиту применив другую схему, это проблема неправильно настроенной авторизации, а не аутентификации. А решение автора крайне не типичное и не расширяемое, и больше вносит путаницы, чем что-то действительно решает.
Говорю из опыта множества проектов с несколькими схемами. Политики решают эту задачу замечательно, потому что они и должны решать такую задачу.
Тут есть странное (с моей точки зрения) поведение.
Допустим, у вас метод аутентификации по умолчанию - Jwt. Если мы не меняли политику авторизации по умолчанию, она равна
new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build()
У политик авторизации есть свойство AuthenticationSchemes
, которое определяет, какие обработчики аутентификации будут запущены. По умолчанию оно пустое. Это значит, что политика будет вызывать обработчик аутентификации по умолчанию, т.е. Jwt.
Неочевидно, что [Authorize(AuthenticationSchemes)]
определяет методы аутентификации в дополнение политике по умолчанию.
И вот что получается с разными вариантами:
[Authorize]
[]
+ []
= []
-> политика авторизации использует метод аутентификации по умолчанию - Jwt.
[Authorize(AuthenticationSchemes = "ApiKey")]
[]
+ ["ApiKey"]
= ["ApiKey"]
-> только метод ApiKey - потому что AuthenticationSchemes
политики больше не пустое, поэтому метод Jwt не используется.
А вот что происходит, если в политике авторизации указать методы аутентификации:
builder.Services.AddAuthorization(options =>
{
options.DefaultPolicy = new AuthorizationPolicyBuilder("Jwt")
.RequireAuthenticatedUser()
.Build();
});
[Authorize]
["Jwt"]
+ []
= ["Jwt"]
-> метод Jwt
[Authorize(AuthenticationSchemes = "ApiKey")]
["Jwt"]
+ ["ApiKey"]
= ["Jwt", "ApiKey"]
-> методы Jwt или ApiKey.
Используем API Key и JWT Bearer аутентификацию вместе в ASP.NET Core Web API