Недавно я столкнулся с задачей реализации аутентификации с использованием API Key в ASP.NET Core Web API. Хотя многие авторы рекомендуют использовать IAuthorizationFilter для этой цели, я обнаружил, что это не самый подходящий вариант. У меня есть более удачный подход, которым я хотел бы поделиться, включая примеры. Реализация была протестирована как в .NET 8, так и в .NET 9.
Почему не стоит использовать IAuthorizationFilter для аутентификации API-ключей?
Использование IAuthorizationFilter для аутентификации API-ключей может показаться простым решением, но оно не является оптимальным. Фильтры авторизации предназначены для обработки логики авторизации, а не аутентификации. Они работают после этапа аутентификации, что означает, что они не могут проверить или установить личность пользователя. Кроме того, использование фильтров для аутентификации может привести к смешению обязанностей, что усложняет расширение и поддержку решения.
Более подходящий подход — реализовать собственный AuthenticationHandler, расширив класс AuthenticationHandler<TOptions>. Это позволяет обрабатывать аутентификацию на уровне middleware, соблюдая принцип разделения обязанностей и предоставляя большую гибкость для расширения.
Реализация аутентификации с использованием API-ключей
Прежде чем приступить к реализации ApiKeyAuthenticationHandler
, давайте обсудим лучшие практики для повышения безопасности и надежности аутентификации с использованием API-ключей.
В простых словах, API-ключ передается с каждым запросом, обычно в заголовке, и сервер проверяет его для предоставления или отказа в доступе. Чтобы эффективно реализовать этот процесс, следуйте следующим рекомендациям:
Используйте криптографически безопасные алгоритмы для генерации API-ключей, чтобы обеспечить высокий уровень случайности. Это снижает риск атак методом перебора.
Всегда передавайте API-ключи через защищенные каналы, такие как HTTPS, чтобы предотвратить их перехват.
Храните API-ключи безопасно. Для приложений с высокой степенью безопасности (например, обработка финансовых или конфиденциальных данных) шифруйте API-ключи в базе данных. Даже если база данных будет скомпрометирована, зашифрованные ключи останутся бесполезными без ключа шифрования.
Регулярно обновляйте API-ключи. Устанавливайте срок действия ключей и ограничивайте количество активных ключей для одного клиента. Определите четкую политику ротации (например, обновление ключей каждые 90 дней).
Логируйте ошибки чтобы быстро выявлять подозрительное поведение.
Никогда не раскрывайте API-ключи в URL, логах или сообщениях об ошибках. Эти места могут быть доступны для посторонних.
Ограничивайте разрешения с помощью областей (scopes). Это ограничивает действия, которые может выполнять API-ключ, снижая риски.
Валидация API-ключей
Первым шагом в этом процессе является создание валидатора API-ключей, который будет отвечать за проверку ключей. Ниже представлен интерфейс для вашей реализации:
public interface IApiKeyValidator
{
/// <summary>
/// Validates the provided API key.
/// </summary>
/// <param name="apiKey">The API key to validate.</param>
/// <returns>
/// A <see cref="Result{UserApiKey}"/> indicating the success or failure of the validation.
/// If successful, returns the associated <see cref="UserApiKey"/> data.
/// If failed, returns an error message.
/// </returns>
Task<Result<UserApiKey?>> Validate(string apiKey);
}
Здесь я использовал паттерн Result для возврата сообщений об ошибках валидации, таких как «токен истек» или аналогичных проблем. Однако вы можете реализовать это по-другому в зависимости от требований вашего приложения.
Реализация Api Key Authentication Handler
Вторым шагом является реализация самого ApiKeyAuthenticationHandler
. Простая реализация обработчика должна включать извлечение API-ключа из запроса (обычно используется заголовок с названием X-API-Key
), его валидацию и идентификацию пользователя, если пользователь авторизован задание контекста. Если ваш API использует области (scopes), обработчик также может устанавливать утверждения политики (policy claims) для обеспечения выполнения политик авторизации. Если валидация не пройдена, запрос должен быть отклонен. Пример реализации приведен ниже:
internal static class ApiKeyAuthenticationDefaults
{
public const string AuthenticationScheme = "ApiKey";
public const string ApiKeyHeaderName = "X-API-Key";
}
internal sealed class ApiKeyAuthenticationHandler(
IOptionsMonitor<AuthenticationSchemeOptions> options,
ILoggerFactory logger,
UrlEncoder encoder,
IApiKeyValidator apiKeyValidator)
: AuthenticationHandler<AuthenticationSchemeOptions>(options, logger, encoder)
{
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
if (!TryGetApiKey(out var apiKey, out var failureMessage))
{
return AuthenticateResult.Fail(failureMessage!);
}
try
{
var result = await apiKeyValidator.Validate(apiKey);
if (!result.IsSuccess)
{
return AuthenticateResult.Fail(result.Error!);
}
var username = result.Value!.UserName;
var claims = new[] {
new Claim(ClaimTypes.NameIdentifier, username),
new Claim(ClaimTypes.Name, username),
new Claim(nameof(username), username)
};
var identity = new ClaimsIdentity(claims, Scheme.Name);
var principal = new ClaimsPrincipal(identity);
var ticket = new AuthenticationTicket(principal, Scheme.Name);
return AuthenticateResult.Success(ticket);
}
catch (Exception ex)
{
failureMessage = "An error occurred during authentication.";
Logger.LogError(ex, failureMessage);
return AuthenticateResult.Fail(failureMessage);
}
}
private bool TryGetApiKey(out string apiKey, out string? failureMessage)
{
apiKey = string.Empty;
if (!Request.Headers.TryGetValue(ApiKeyAuthenticationDefaults.ApiKeyHeaderName, out var headerValues))
{
failureMessage = $"Missing '{ApiKeyAuthenticationDefaults.ApiKeyHeaderName}' header.";
return false;
}
if (headerValues.Count != 1)
{
failureMessage = $"Expecting only a single '{ApiKeyAuthenticationDefaults.ApiKeyHeaderName}' header.";
return false;
}
apiKey = headerValues.FirstOrDefault() ?? string.Empty;
if (string.IsNullOrWhiteSpace(apiKey))
{
failureMessage = $"'{ApiKeyAuthenticationDefaults.ApiKeyHeaderName}' header value is null or empty.";
return false;
}
failureMessage = null;
return true;
}
}
Кроме того, переопределите метод HandleChallengeAsync
. Когда вызывается метод Challenge
, сервер должен ответить с HTTP-статусом (обычно 401 Unauthorized) и включить дополнительную информацию, такую как заголовок WWW-Authenticate
. Этот заголовок информирует клиент о требуемой схеме аутентификации.
Например, если API-ключ отсутствует или недействителен, сервер может запросить у клиента предоставление действительного API-ключа, вернув ответ 401 с заголовком, похожим на:
WWW-Authenticate: ApiKey
Для улучшения дизайна API рассмотрите возможность использования стандартизированного подхода, возвращая problem details. Это помогает клиентам понять проблемы в структурированном виде. Ниже приведена реализация:
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var authResult = await HandleAuthenticateOnceAsync();
if (authResult.Succeeded)
{
return;
}
Response.StatusCode = StatusCodes.Status401Unauthorized;
Response.Headers.WWWAuthenticate = ApiKeyAuthenticationDefaults.AuthenticationScheme;
var detail = authResult.Failure?.Message;
const string type = "https://tools.ietf.org/html/rfc9110#section-15.5.2";
var problemDetails = new ProblemDetails()
{
Type = type,
Title = ReasonPhrases.GetReasonPhrase(StatusCodes.Status401Unauthorized),
Status = StatusCodes.Status401Unauthorized,
Detail = detail
};
const string contentType = "application/problem+json";
await Response.WriteAsJsonAsync(problemDetails, (JsonSerializerOptions?)null, contentType);
}
Здесь, если вы ранее настроили параметры JSON для API, метод WriteAsJsonAsync
будет использовать эти настройки вместо стандартных.
Пример ответа на запрос с неавторизованным доступом:

Настройка схемы аутентификации
Наконец, вам нужно зарегистрировать обработчик и настроить схему аутентификации:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = ApiKeyAuthenticationDefaults.AuthenticationScheme;
}).AddScheme<AuthenticationSchemeOptions, ApiKeyAuthenticationHandler>(ApiKeyAuthenticationDefaults.AuthenticationScheme, _ => { });
builder.Services.AddAuthorization();
builder.Services.AddScoped<IApiKeyValidator, ApiKeyValidator>();
var app = builder.Build();
Реализуя обработчик ApiKeyAuthenticationHandler
, вы можете легко добавить аутентификацию на основе API-ключей в ваше приложение ASP.NET Core — как вместе с другими схемами аутентификации, так и как отдельную схему. Этот подход предоставляет гибкость и контроль над тем, как вы защищаете ваши API. Просто не забывайте следовать лучшим практикам для безопасной обработки и хранения API-ключей.
Надеюсь, этот гайд окажется полезным! Если у вас есть мысли или предложения, не стесняйтесь поделиться ими в комментариях. Спасибо!
P.S.: В следующем посте я планирую поделиться тем, как использовать и JWT, и аутентификацию по API-ключу на одном и том же API-эндпоинте. Это может быть особенно полезно, если вы хотите использовать JWT для аутентификации пользователей, а API-ключ для аутентификации между сервисами. Дайте знать в комментариях, если хотите увидеть этот пост!
Marsezi
Слишком много кода, условий , инициализации , типизации под схемы. В отличии от 1 метода в 5 строк кода мидлвара с чеком например в редис