Нещодавно я зіткнувся з викликом впровадження автентифікації (Authentication) за допомогою API-ключів (API Key) у веб-API на базі ASP.NET Core. Хоча багато авторів рекомендують використовувати IAuthorizationFilter
для цієї мети, я вважаю цей підхід не найкращим. У мене є кращий спосіб, і я хотів би поділитися ним разом із прикладами. Реалізація була протестована як на .NET 8, так і на .NET 9, щоб забезпечити сумісність і надійність.
Чому не варто використовувати IAuthorizationFilter для автентифікації за API-ключами?
Використання IAuthorizationFilter
для автентифікації (Authentication) за допомогою API-ключів може здатися простим, але це не найкращий підхід. Фільтри авторизації (Authorization Filters) призначені для обробки логіки авторизації (Authorization), а не автентифікації (Authentication). Вони працюють після фази автентифікації, тобто не можуть перевірити або встановити ідентичність користувача. Крім того, використання фільтрів для автентифікації (Authentication) може заплутати розподіл відповідальностей, що ускладнить розширення та підтримку рішення.
Кращим підходом є створення кастомного AuthenticationHandler
, успадкувавши його від AuthenticationHandler
. Це дозволяє обробляти автентифікацію (Authentication) на рівні проміжного програмного забезпечення (Middleware), дотримуючись принципу розподілу обов’язків і забезпечуючи більшу гнучкість для розширення.
Впровадження автентифікації за API-ключами
Перш ніж реалізовувати ApiKeyAuthenticationHandler
, обговорімо найкращі практики для підвищення безпеки та надійності вашої реалізації автентифікації (Authentication) за API-ключами.
Простими словами, API-ключ (API Key) передається з кожним запитом, зазвичай у заголовку (Header), і сервер перевіряє його, щоб надати або відмовити в доступі. Для ефективної реалізації цього підходу важливо дотримуватися наступних рекомендацій щодо безпеки:
- Використовуйте криптографічно захищені алгоритми для генерації API-ключів, забезпечуючи високу якість випадковості. Це зменшує ризик атак методом підбору (brute-force).
- Завжди передавайте API-ключі через захищені канали, такі як HTTPS, щоб запобігти їх перехопленню.
- Зберігайте API-ключі в безпеці. Для додатків із високим рівнем безпеки (наприклад, для роботи з фінансовими чи чутливими даними) шифруйте API-ключі в базі даних. Навіть якщо базу даних буде зламано, зашифровані ключі залишаться непридатними без ключа розшифрування. Автоматизуйте повторне шифрування з використанням оновлених ключів шифрування за необхідності.
- Регулярно змінюйте API-ключі. Встановіть дату закінчення терміну дії та дозволяйте лише обмежену кількість активних ключів на клієнта. Визначте чітку політику ротації ключів (наприклад, оновлення ключів кожні 90 днів).
- Ведіть детальні журнали аудиту використання API-ключів, щоб відстежувати активність і швидко виявляти підозрілу поведінку.
- Ніколи не розкривайте API-ключі в URL-адресах, журналах або повідомленнях про помилки. Ці місця легко доступні для сторонніх осіб.
- Обмежуйте дозволи (permissions) за допомогою сфер доступу (scopes). Це дозволяє використовувати API-ключі лише для конкретних дій, зменшуючи ризики.
Перевірка API-ключів
Перший крок у цьому процесі — створити Валідатор API-ключів (API Key Validator), який займатиметься перевіркою ключів. Нижче наведено інтерфейс для вашої реалізації:
public interface IApiKeyValidator
{
///
/// Validates the provided API key. /// /// The API key to validate. /// /// A indicating the success or failure of the validation. /// If successful, returns the associated data. /// If failed, returns an error message. /// Task> Validate(string apiKey); } ```
Тут я використав патерн Result для повернення повідомлень про помилки валідації, наприклад, «токен закінчився» або подібних проблем. Однак ви можете застосувати інший підхід відповідно до вимог вашого застосунку.
## **Реалізація ApiKeyAuthenticationHandler**
Другий крок — реалізувати сам ApiKeyAuthenticationHandler. Проста реалізація обробника має включати отримання API-ключа з запиту (загальноприйнята конвенція — використання заголовка X-API-Key), його перевірку та встановлення ідентичності користувача в контексті, якщо користувач авторизований.
Якщо ваш API використовує області доступу (scopes), обробник (handler) також може встановлювати політики вимог (policy claims), щоб забезпечити дотримання політик авторизації (Authorization). Якщо перевірка завершується невдачею, запит має бути відхилений. Реалізація показана нижче:
internal static class ApiKeyAuthenticationDefaults
{
public const string AuthenticationScheme = "ApiKey";
public const string ApiKeyHeaderName = "X-API-Key";
}
internal sealed class ApiKeyAuthenticationHandler(
IOptionsMonitor options,
ILoggerFactory logger,
UrlEncoder encoder,
IApiKeyValidator apiKeyValidator)
: AuthenticationHandler(options, logger, encoder)
{
protected override async Task 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
. Цей заголовок інформує клієнта про необхідну схему автентифікації (Authentication Scheme).
Наприклад, якщо 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
використовуватиме ці параметри замість стандартних.
Приклад відповіді про неавторизований доступ:
Налаштування кастомної схеми автентифікації
Нарешті, потрібно зареєструвати кастомний обробник автентифікації (Authentication Handler) та налаштувати схеми автентифікації:
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(options =>
{
options.DefaultScheme = ApiKeyAuthenticationDefaults.AuthenticationScheme;
}).AddScheme(ApiKeyAuthenticationDefaults.AuthenticationScheme, _ => { });
builder.Services.AddAuthorization();
builder.Services.AddScoped();
var app = builder.Build();
Реалізувавши кастомний ApiKeyAuthenticationHandler
, ви легко зможете додати автентифікацію (Authentication) на основі API-ключів до вашого застосунку на базі ASP.NET Core — як окремо, так і разом з іншими схемами автентифікації. Такий підхід забезпечує гнучкість та контроль над тим, як ви захищаєте свої API. Не забувайте дотримуватися найкращих практик у роботі з API-ключами, включаючи їх зберігання та обробку.
Сподіваюся, цей гайд буде корисним! Якщо у вас є думки чи пропозиції, будь ласка, поділіться ними в коментарях. Дякую!
P.S.: У моєму наступному дописі я планую поділитися тим, як використовувати JWT і API Key Authentication на одному й тому ж API-ендпоінті (API Endpoint). Це може бути особливо корисним, якщо ви хочете використовувати JWT для автентифікації користувачів, а API-ключ — для автентифікації між сервісами (Service-to-Service Authentication). Це може бути трохи складно. Дайте знати в коментарях, якщо вам цікаво побачити цей допис!
Перекладено з: API Key Authentication in ASP.NET Core Web Api