Как стать автором
Обновить

Комментарии 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 атрибут становится бесполезен так как пропускает обе схемы все равно.


И на всякий случай советую проверить везде где вы это используете, так как, это может непреднамеренно привести к несанкционированному доступу, если 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.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории