Фильтры проверки подлинности в веб-API ASP.NET 2

Майк Уосон

Фильтр проверки подлинности — это компонент, который проверяет подлинность HTTP-запроса. Веб-API 2 и MVC 5 поддерживают фильтры проверки подлинности, но они немного отличаются, в основном в соглашениях об именовании для интерфейса фильтра. В этом разделе описываются фильтры проверки подлинности веб-API.

Фильтры проверки подлинности позволяют задать схему проверки подлинности для отдельных контроллеров или действий. Таким образом, приложение может поддерживать различные механизмы проверки подлинности для разных ресурсов HTTP.

В этой статье я покажем код из примера обычной проверки подлинности на https://github.com/aspnet/samples. В примере показан фильтр проверки подлинности, реализующий схему проверки подлинности http basic access (RFC 2617). Фильтр реализуется в классе с именем IdentityBasicAuthenticationAttribute. Я не буду показывать весь код из примера, а только те части, которые иллюстрируют, как написать фильтр проверки подлинности.

Настройка фильтра проверки подлинности

Как и другие фильтры, фильтры проверки подлинности можно применять для каждого контроллера, каждого действия или глобально ко всем контроллерам веб-API.

Чтобы применить фильтр проверки подлинности к контроллеру, украсите класс контроллера атрибутом filter. Следующий код задает [IdentityBasicAuthentication] фильтр для класса контроллера, который включает обычную проверку подлинности для всех действий контроллера.

[IdentityBasicAuthentication] // Enable Basic authentication for this controller.
[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }
    public IHttpActionResult Post() { . . . }
}

Чтобы применить фильтр к одному действию, украсите его фильтром. Следующий код задает [IdentityBasicAuthentication] фильтр для метода контроллера Post .

[Authorize] // Require authenticated requests.
public class HomeController : ApiController
{
    public IHttpActionResult Get() { . . . }

    [IdentityBasicAuthentication] // Enable Basic authentication for this action.
    public IHttpActionResult Post() { . . . }
}

Чтобы применить фильтр ко всем контроллерам веб-API, добавьте его в GlobalConfiguration.Filters.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.Filters.Add(new IdentityBasicAuthenticationAttribute());

        // Other configuration code not shown...
    }
}

Реализация фильтра проверки подлинности веб-API

В веб-API фильтры проверки подлинности реализуют интерфейс System.Web.Http.Filters.IAuthenticationFilter . Они также должны наследоваться от System.Attribute, чтобы применяться в качестве атрибутов.

Интерфейс IAuthenticationFilter имеет два метода:

  • AuthenticateAsync проверяет подлинность запроса, проверяя учетные данные в запросе, если они есть.
  • При необходимости ChallengeAsync добавляет запрос проверки подлинности в HTTP-ответ.

Эти методы соответствуют потоку проверки подлинности, определенному в RFC 2612 и RFC 2617:

  1. Клиент отправляет учетные данные в заголовке авторизации. Обычно это происходит после того, как клиент получает ответ 401 (несанкционированный) от сервера. Однако клиент может отправить учетные данные с любым запросом, а не только после получения 401.
  2. Если сервер не принимает учетные данные, он возвращает ответ 401 (не авторизовано). Ответ содержит заголовок Www-Authenticate, содержащий один или несколько задач. Каждый запрос определяет схему проверки подлинности, распознаваемую сервером.

Сервер также может вернуть 401 из анонимного запроса. Обычно именно так инициируется процесс проверки подлинности:

  1. Клиент отправляет анонимный запрос.
  2. Сервер возвращает 401.
  3. Клиенты повторно отправляет запрос с учетными данными.

Этот поток включает в себя как этапы проверки подлинности , так и авторизации .

  • Проверка подлинности подтверждает удостоверение клиента.
  • Авторизация определяет, может ли клиент получить доступ к определенному ресурсу.

В веб-API фильтры проверки подлинности обрабатывают проверку подлинности, но не авторизацию. Авторизация должна выполняться фильтром авторизации или внутри действия контроллера.

Ниже приведен поток в конвейере веб-API 2:

  1. Перед вызовом действия веб-API создает список фильтров проверки подлинности для этого действия. Сюда входят фильтры с область действий, область контроллера и глобальные область.
  2. Веб-API вызывает AuthenticateAsync для каждого фильтра в списке. Каждый фильтр может проверять учетные данные в запросе. Если какой-либо фильтр успешно проверяет учетные данные, фильтр создает IPrincipal и присоединяет его к запросу. На этом этапе фильтр также может вызвать ошибку. В этом случае остальная часть конвейера не выполняется.
  3. Если ошибка отсутствует, запрос проходит через остальную часть конвейера.
  4. Наконец, веб-API вызывает метод ChallengeAsync каждого фильтра проверки подлинности. Фильтры используют этот метод для добавления запроса в ответ, если это необходимо. Обычно (но не всегда) это происходит в ответ на ошибку 401.

На следующих схемах показаны два возможных варианта. Во-первых, фильтр проверки подлинности успешно проверяет подлинность запроса, фильтр авторизации авторизует запрос, а действие контроллера возвращает значение 200 (ОК).

Схема успешной проверки подлинности

Во втором примере фильтр проверки подлинности проверяет подлинность запроса, но фильтр авторизации возвращает значение 401 (Не авторизовано). В этом случае действие контроллера не вызывается. Фильтр проверки подлинности добавляет в ответ заголовок Www-Authenticate.

Схема несанкционированной проверки подлинности

Возможны и другие сочетания. Например, если действие контроллера разрешает анонимные запросы, у вас может быть фильтр проверки подлинности, но без авторизации.

Реализация метода AuthenticateAsync

Метод AuthenticateAsync пытается проверить подлинность запроса. Вот так выглядит подпись метода.

Task AuthenticateAsync(
    HttpAuthenticationContext context,
    CancellationToken cancellationToken
)

Метод AuthenticateAsync должен выполнить одно из следующих действий:

  1. Ничего (без операции).
  2. Создайте IPrincipal и задайте его в запросе.
  3. Задайте результат ошибки.

Параметр (1) означает, что у запроса не было учетных данных, понятных фильтру. Параметр (2) означает, что фильтр успешно прошел проверку подлинности запроса. Вариант (3) означает, что запрос содержит недопустимые учетные данные (например, неправильный пароль), что приводит к возникновению ошибки.

Ниже приведены общие сведения о реализации AuthenticateAsync.

  1. Найдите учетные данные в запросе.
  2. Если учетные данные отсутствуют, ничего не делать и возвращать (no-op).
  3. Если есть учетные данные, но фильтр не распознает схему проверки подлинности, ничего не делать и возвращать (no-op). Схема может быть понята другому фильтру в конвейере.
  4. Если фильтр распознает учетные данные, попробуйте проверить их подлинность.
  5. Если учетные данные неверны, верните значение 401, задав .context.ErrorResult
  6. Если учетные данные действительны, создайте IPrincipal и задайте .context.Principal

В следующем коде показан метод AuthenticateAsync из примера обычной проверки подлинности . В комментариях указывается каждый шаг. В коде отображается несколько типов ошибок: заголовок авторизации без учетных данных, неправильно сформированные учетные данные и неправильное имя пользователя и пароль.

public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
{
    // 1. Look for credentials in the request.
    HttpRequestMessage request = context.Request;
    AuthenticationHeaderValue authorization = request.Headers.Authorization;

    // 2. If there are no credentials, do nothing.
    if (authorization == null)
    {
        return;
    }

    // 3. If there are credentials but the filter does not recognize the 
    //    authentication scheme, do nothing.
    if (authorization.Scheme != "Basic")
    {
        return;
    }

    // 4. If there are credentials that the filter understands, try to validate them.
    // 5. If the credentials are bad, set the error result.
    if (String.IsNullOrEmpty(authorization.Parameter))
    {
        context.ErrorResult = new AuthenticationFailureResult("Missing credentials", request);
        return;
    }

    Tuple<string, string> userNameAndPassword = ExtractUserNameAndPassword(authorization.Parameter);
    if (userNameAndPassword == null)
    {
        context.ErrorResult = new AuthenticationFailureResult("Invalid credentials", request);
    }

    string userName = userNameAndPassword.Item1;
    string password = userNameAndPassword.Item2;

    IPrincipal principal = await AuthenticateAsync(userName, password, cancellationToken);
    if (principal == null)
    {
        context.ErrorResult = new AuthenticationFailureResult("Invalid username or password", request);
    }

    // 6. If the credentials are valid, set principal.
    else
    {
        context.Principal = principal;
    }

}

Задание результата ошибки

Если учетные данные недопустимы, фильтр должен задать значение context.ErrorResultIHttpActionResult , которое создает ответ об ошибке. Дополнительные сведения об IHttpActionResult см. в разделе Результаты действий в веб-API 2.

Пример обычной AuthenticationFailureResult проверки подлинности включает класс, который подходит для этой цели.

public class AuthenticationFailureResult : IHttpActionResult
{
    public AuthenticationFailureResult(string reasonPhrase, HttpRequestMessage request)
    {
        ReasonPhrase = reasonPhrase;
        Request = request;
    }

    public string ReasonPhrase { get; private set; }

    public HttpRequestMessage Request { get; private set; }

    public Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        return Task.FromResult(Execute());
    }

    private HttpResponseMessage Execute()
    {
        HttpResponseMessage response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
        response.RequestMessage = Request;
        response.ReasonPhrase = ReasonPhrase;
        return response;
    }
}

Реализация ChallengeAsync

Цель метода ChallengeAsync — добавить запросы проверки подлинности в ответ, если это необходимо. Вот так выглядит подпись метода.

Task ChallengeAsync(
    HttpAuthenticationChallengeContext context,
    CancellationToken cancellationToken
)

Метод вызывается для каждого фильтра проверки подлинности в конвейере запросов.

Важно понимать, что ChallengeAsync вызывается до создания HTTP-ответа и, возможно, даже до выполнения действия контроллера. При вызове context.ResultChallengeAsync содержит IHttpActionResult, который используется позже для создания HTTP-ответа. Поэтому при вызове ChallengeAsync вы еще ничего не знаете о HTTP-ответе. Метод ChallengeAsync должен заменить исходное значение новым значением context.ResultIHttpActionResult. Для этого объекта IHttpActionResult необходимо создать оболочку исходного context.Resultобъекта .

Схема ChallengeAsync

Я назову исходный результат IHttpActionResultвнутренним результатом, а новый внешним результатом. Внешний результат должен выполнять следующие действия:

  1. Вызовите внутренний результат, чтобы создать HTTP-ответ.
  2. Изучите ответ.
  3. При необходимости добавьте запрос проверки подлинности в ответ.

Следующий пример взят из примера обычной проверки подлинности. Он определяет IHttpActionResult для внешнего результата.

public class AddChallengeOnUnauthorizedResult : IHttpActionResult
{
    public AddChallengeOnUnauthorizedResult(AuthenticationHeaderValue challenge, IHttpActionResult innerResult)
    {
        Challenge = challenge;
        InnerResult = innerResult;
    }

    public AuthenticationHeaderValue Challenge { get; private set; }

    public IHttpActionResult InnerResult { get; private set; }

    public async Task<HttpResponseMessage> ExecuteAsync(CancellationToken cancellationToken)
    {
        HttpResponseMessage response = await InnerResult.ExecuteAsync(cancellationToken);

        if (response.StatusCode == HttpStatusCode.Unauthorized)
        {
            // Only add one challenge per authentication scheme.
            if (!response.Headers.WwwAuthenticate.Any((h) => h.Scheme == Challenge.Scheme))
            {
                response.Headers.WwwAuthenticate.Add(Challenge);
            }
        }

        return response;
    }
}

Свойство InnerResult содержит внутренний объект IHttpActionResult. Свойство Challenge представляет заголовок Www-Authentication. Обратите внимание, что ExecuteAsync сначала вызывает InnerResult.ExecuteAsync для создания HTTP-ответа, а затем при необходимости добавляет запрос.

Проверьте код ответа перед добавлением запроса. Большинство схем проверки подлинности добавляют запрос, только если ответ равен 401, как показано ниже. Однако некоторые схемы проверки подлинности добавляют вызов к успешному ответу. Например, см . раздел Согласование (RFC 4559).

С учетом AddChallengeOnUnauthorizedResult класса фактический код в ChallengeAsync прост. Просто создайте результат и прикрепите его к context.Result.

public Task ChallengeAsync(HttpAuthenticationChallengeContext context, CancellationToken cancellationToken)
{
    var challenge = new AuthenticationHeaderValue("Basic");
    context.Result = new AddChallengeOnUnauthorizedResult(challenge, context.Result);
    return Task.FromResult(0);
}

Примечание. Пример обычной проверки подлинности немного абстрагирует эту логику, помещая ее в метод расширения.

Объединение фильтров проверки подлинности с проверкой подлинности Host-Level

"Проверка подлинности на уровне узла" — это проверка подлинности, выполняемая узлом (например, IIS) до того, как запрос достигнет платформы веб-API.

Часто может потребоваться включить проверку подлинности на уровне узла для остальной части приложения, но отключить ее для контроллеров веб-API. Например, типичный сценарий — включение проверки подлинности с помощью форм на уровне узла, но использование проверки подлинности на основе маркеров для веб-API.

Чтобы отключить проверку подлинности на уровне узла в конвейере веб-API, вызовите config.SuppressHostPrincipal() в конфигурации. Это приводит к тому, что веб-API удаляет IPrincipal из любого запроса, который входит в конвейер веб-API. Фактически он "отменяет проверку подлинности" запроса.

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        config.SuppressHostPrincipal();

        // Other configuration code not shown...
    }
}

Дополнительные ресурсы

Фильтры безопасности веб-API ASP.NET (журнал MSDN)