Я расскажу, как реализовать аутентификацию с использованием как JWT, так и API-ключа на одном и том же endpoint в ASP.NET Core Web API. Совмещение этих схем аутентификации полезно, если вы хотите использовать токен JWT Bearer для аутентификации пользователей и API-ключ для аутентификации между сервисами.
![](https://habrastorage.org/getpro/habr/upload_files/6f4/1de/f11/6f41def11db2f5cb0092a0cba8ab3b71.jpg)
Эта статья основана на моем предыдущей статье, в которой я уже рассмотрел аутентификацию с использованием API-ключа в ASP.NET Core. Если вам нужно подробное объяснение этой реализации, обратитесь к ней. Эта реализация была протестирована как в .NET 8, так и в .NET 9.
ASP.NET Core позволяет комбинировать обработчики аутентификации для поддержки нескольких схем, и я продемонстрирую, как это реализовать.
Реализация составного обработчика аутентификации
Для того чтобы включить аутентификацию как с использованием JWT, так и с использованием API-ключа на одном endpoint, нам нужен составной обработчик аутентификации. Этот обработчик определяет, какую схему аутентификации применить в зависимости от запроса. Сначала мы проверим, содержит ли запрос API-ключ; если нет, мы применим схему аутентификации с использованием JWT. Реализация показана ниже:
internal static class CompositeAuthenticationDefaults
{
public const string AuthenticationScheme =
$"Composite-{JwtBearerDefaults.AuthenticationScheme}-{ApiKeyAuthenticationDefaults.ApiKeyHeaderName}";
}
internal sealed class CompositeAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var scheme = GetAuthunticationScheme();
return await Context.AuthenticateAsync(scheme);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var scheme = GetAuthunticationScheme();
await Context.ChallengeAsync(scheme);
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
var scheme = GetAuthunticationScheme();
await Context.ForbidAsync(scheme);
}
private bool IsApiKeyAuthScheme()
=> Request.Headers.ContainsKey(ApiKeyAuthenticationDefaults.ApiKeyHeaderName);
private string GetAuthunticationScheme()
=> IsApiKeyAuthScheme()
? ApiKeyAuthenticationDefaults.AuthenticationScheme
: JwtBearerDefaults.AuthenticationScheme;
}
Настройка составной схемы аутентификации
Далее нам нужно настроить наши службы аутентификации для использования как JWT, так и API-ключа в файле Program.cs:
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = CompositeAuthenticationDefaults.AuthenticationScheme;
})
.AddScheme<AuthenticationSchemeOptions, CompositeAuthenticationHandler>(CompositeAuthenticationDefaults.AuthenticationScheme, _ => { })
.AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, _ => { })
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
{
options.Authority = "authority";
options.MetadataAddress = "metadata-address"; // e.g. https://login.microsoftonline.com/your-tenant-id/v2.0/.well-known/openid-configuration
options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
};
});
builder.Services.AddAuthorization();
В этом примере конфигурация JWT гарантирует, что токены выданы доверенным провайдером и проверяет ключевые аспекты безопасности, такие как срок действия и ключ подписи.
Чтобы добавить аутентификацию JWT в ваш проект, установите пакет Microsoft.AspNetCore.Authentication.JwtBearer
из NuGet.
Применение схем аутентификации в контроллерах
Для защиты API с использованием обеих схем аутентификации примените атрибут [Authorize]
следующим образом:
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class SecureController : ControllerBase
{
[HttpGet]
public IActionResult GetSecureData()
{
return Ok(new { Message = "This is a secure endpoint." });
}
}
Если вы хотите ограничить endpoint использованием только JWT схемы, укажите:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
Аналогично для API Key:
[Authorize(AuthenticationSchemes = ApiKeyAuthenticationDefaults.AuthenticationScheme)]
Ниже пример использования [Authorize]
атрибута в minimal API:
app.MapGet("/api/secure", [Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)] () =>
{
return new { Message = "This is a secure endpoint." };
}).RequireAuthorization();
Заключение
Реализовав свой составной обработчик аутентификации, мы можем без проблем поддерживать как JWT, так и аутентификацию с использованием API-ключа на одном и том же API endpoint. Этот подход предоставляет гибкость для сценариев, когда различные пользователи вашего API требуют разных методов аутентификации.
Если вы нашли это руководство полезным или у вас есть вопросы, не стесняйтесь оставить комментарий!
Дополнение в ответ на комментарий: Использование AuthorizationPolicyBuilder
для установки политики по умолчанию, как в коде ниже:
builder.Services.AddAuthorizationBuilder()
.SetDefaultPolicy(new AuthorizationPolicyBuilder(ApiKeyAuthenticationDefaults.AuthenticationScheme, JwtBearerDefaults.AuthenticationScheme)
.RequireAuthenticatedUser()
.Build());
не подходит, если вы хотите применять только одну схему аутентификации для конкретного API-эндпоинта, поскольку устанавливает политику авторизации по умолчанию, которая допускает аутентификацию либо через API-ключ, либо через JWT для всех защищенных эндпоинтов. Это делает невозможным ограничение эндпоинта только одной схемой (например, только JWT или только API-ключ). Это может непреднамеренно привести к несанкционированному доступу, если эндпоинт должен поддерживать только один метод аутентификации. Например, если эндпоинт предназначен для аутентификации через JWT, но по умолчанию также разрешает API-ключ, это может создать угрозу безопасности, позволяя API-ключу обходить более строгие проверки аутентификации.
withkittens
Мне кажется, ваше решение сильно переусложнено. И есть несколько ошибок.
Давайте сначала разберём
ApiKeyAuthenticationHandler
из первой части:Когда схеме аутентификации недостаточно данных (например, заголовок с API-ключом отсутствует вообще), она должна возвращать
AuthenticationResult.NoResult()
. Это нужно чтобы не заваливать аутентификацию целиком, а дать другим схемам возможность тоже отработать. О, это же как раз наш случай!AuthenticationResult.Fail(...)
- это для случаев, когда схема уже однозначно определена, например, если в запросе передан нужный заголовок, но в нём нет ключа или он невалидный:ApiKeyAuthenticationHandler.cs
Теперь посмотрим на
CompositeAuthenticationHandler
. Он не нужен :) Нужно настроить политику авторизации:Тогда сначала произойдёт попытка аутентифицироваться по ключу, а если не получилось, то по токену. Конечно же, атрибут
[Authorize(AuthenticationSchemes = ...)]
с указанием конкретных схем продолжит работать.AntonAntonov88 Автор
Да вы правы, проверил, не поспотрел в сторону
AuthorizationPolicyBuilder
сразу потому что у меня Composite немного сложнее чем в статье, я добавляю WWW-Authenticate в ответ для каждой поддерживаемой схемы чтобы информировать пользователя о необходиости использовать ApiKey. Спасибо, статью уберу в архив, она тогда получается имеет мало смысла)AntonAntonov88 Автор
Еще раз перепроверил:
И результат такой что я могу использовать ApiKey, т.е. Authorize атрибут становится бесполезен так как пропускает обе схемы все равно.
AntonAntonov88 Автор
The
AuthorizationPolicyBuilder
is used to build a policy that requires authentication using one of the provided authentication schemes.AntonAntonov88 Автор
И на всякий случай советую проверить везде где вы это используете, так как, это может непреднамеренно привести к несанкционированному доступу, если API должна поддерживать только один метод аутентификации. Например, если конечная точка предназначена для аутентификации с помощью JWT, но по умолчанию также разрешает аутентификацию с использованием API-ключа, это может подвергнуть endpoint рискам безопасности, при которых API-ключ может обойти более строгие проверки аутентификации.
withkittens
Хм, да, вы правы. Если в
[Authorize]
не указыватьPolicy
, берётся политика по умолчанию, а мы в ней уже разрешили оба метода аутентификации.Но тогда получается всё ещё проще: политику авторизации по умолчанию мы не трогаем (или по крайней мере не указываем методы аутентификации):
и перечисляем требуемые методы в
[Authorize]
:AntonAntonov88 Автор
Т.е. там где указано как вы пишите
// Только ApiKey[Authorize(AuthenticationSchemes = "ApiKey")]
Но если политика по умолчанию Jwt, то все равно Jwt будет работать, что противоречит ожидаемому поведению, не так ли?
AntonAntonov88 Автор
Я пишу про проверки методов аутентификации а вы пытаетесь решить это с помощью политик авторизации.
withkittens
Верно - потому что пропускать или запрещать запросы - это ответственность авторизации, отдельного шага после аутентификации.
Грубо говоря, аутентификация проверяет у пользователя документы, это может быть паспорт или водительские права, а авторизация решает, какие именно документы (уже проверенные к этому моменту) принимать - любые или, например, только паспорт.
withkittens
Тут есть странное (с моей точки зрения) поведение.
Допустим, у вас метод аутентификации по умолчанию - Jwt. Если мы не меняли политику авторизации по умолчанию, она равна
У политик авторизации есть свойство
AuthenticationSchemes
, которое определяет, какие обработчики аутентификации будут запущены. По умолчанию оно пустое. Это значит, что политика будет вызывать обработчик аутентификации по умолчанию, т.е. Jwt.Неочевидно, что
[Authorize(AuthenticationSchemes)]
определяет методы аутентификации в дополнение политике по умолчанию.И вот что получается с разными вариантами:
[]
+[]
=[]
-> политика авторизации использует метод аутентификации по умолчанию - Jwt.[]
+["ApiKey"]
=["ApiKey"]
-> только метод ApiKey - потому чтоAuthenticationSchemes
политики больше не пустое, поэтому метод Jwt не используется.А вот что происходит, если в политике авторизации указать методы аутентификации:
["Jwt"]
+[]
=["Jwt"]
-> метод Jwt["Jwt"]
+["ApiKey"]
=["Jwt", "ApiKey"]
-> методы Jwt или ApiKey.