Как защитить веб-API ASP.NET [закрыто]

397

Я хочу создать веб-сервис RESTful с использованием веб-API ASP.NET, который сторонние разработчики будут использовать для доступа к данным моего приложения.

Я много читал об OAuth и он, кажется, является стандартом, но найти хороший образец с документацией, объясняющей, как это работает (и на самом деле работает!), Кажется невероятно трудным (особенно для новичка в OAuth).

Есть ли пример, который на самом деле строит и работает и показывает, как это реализовать?

Я скачал множество образцов:

  • DotNetOAuth - документация безнадежна с точки зрения новичка
  • Thinktecture - не могу заставить его строить

Я также посмотрел на блоги, предлагающие простую схему на основе токенов (например, такую ) - это похоже на повторное изобретение колеса, но у него есть преимущество в концептуальной простоте.

Похоже, что на SO так много вопросов, но нет хороших ответов.

Что все делают в этом пространстве?

Крейг Ширер
источник

Ответы:

292

Обновить:

Я добавил эту ссылку в свой другой ответ, как использовать аутентификацию JWT для ASP.NET Web API здесь для всех, кто интересуется JWT.


Нам удалось применить аутентификацию HMAC для защиты веб-API, и все заработало нормально. HMAC-аутентификация использует секретный ключ для каждого потребителя, который, как пользователь, так и сервер, знают, что hmac хэширует сообщение, следует использовать HMAC256. В большинстве случаев хешированный пароль потребителя используется в качестве секретного ключа.

Сообщение обычно строится из данных в HTTP-запросе или даже из пользовательских данных, которые добавляются в HTTP-заголовок. Сообщение может содержать:

  1. Отметка времени: время отправки запроса (UTC или GMT)
  2. HTTP-глагол: GET, POST, PUT, DELETE.
  3. разместить данные и строку запроса,
  4. URL

Под капотом аутентификация HMAC будет:

Потребитель отправляет HTTP-запрос на веб-сервер, после построения подписи (вывод хеша hmac) шаблон HTTP-запроса:

User-Agent: {agent}   
Host: {host}   
Timestamp: {timestamp}
Authentication: {username}:{signature}

Пример для запроса GET:

GET /webapi.hmac/api/values

User-Agent: Fiddler    
Host: localhost    
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

Сообщение для хеширования для получения подписи:

GET\n
Thursday, August 02, 2012 3:30:32 PM\n
/webapi.hmac/api/values\n

Пример запроса POST со строкой запроса (подпись ниже не верна, просто пример)

POST /webapi.hmac/api/values?key2=value2

User-Agent: Fiddler    
Host: localhost    
Content-Type: application/x-www-form-urlencoded
Timestamp: Thursday, August 02, 2012 3:30:32 PM 
Authentication: cuongle:LohrhqqoDy6PhLrHAXi7dUVACyJZilQtlDzNbLqzXlw=

key1=value1&key3=value3

Сообщение в хеш для получения подписи

GET\n
Thursday, August 02, 2012 3:30:32 PM\n
/webapi.hmac/api/values\n
key1=value1&key2=value2&key3=value3

Обратите внимание, что данные формы и строка запроса должны располагаться по порядку, поэтому код на сервере получает строку запроса и данные формы для построения правильного сообщения.

Когда на сервер поступает HTTP-запрос, для анализа запроса внедряется фильтр действий аутентификации: HTTP-глагол, временная метка, uri, данные формы и строка запроса, а затем на их основе создается сигнатура (используется хэш hmac) с секретом. ключ (хешированный пароль) на сервере.

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

Затем код сервера сравнивает подпись на запросе с созданной подписью; если равно, аутентификация пройдена, в противном случае она не удалась.

Код для построения подписи:

private static string ComputeHash(string hashedPassword, string message)
{
    var key = Encoding.UTF8.GetBytes(hashedPassword.ToUpper());
    string hashString;

    using (var hmac = new HMACSHA256(key))
    {
        var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
        hashString = Convert.ToBase64String(hash);
    }

    return hashString;
}

Итак, как предотвратить повторную атаку?

Добавьте ограничение для отметки времени, что-то вроде:

servertime - X minutes|seconds  <= timestamp <= servertime + X minutes|seconds 

(время сервера: время поступления запроса на сервер)

И, кешировать подпись запроса в памяти (используйте MemoryCache, следует ограничивать время). Если следующий запрос приходит с той же подписью, что и предыдущий, он будет отклонен.

Демо-код приведен здесь: https://github.com/cuongle/Hmac.WebApi

cuongle
источник
2
@James: только отметка времени кажется недостаточной, в течение короткого времени они могут имитировать запрос и отправлять его на сервер, я только что отредактировал свое сообщение, лучше использовать оба.
cuongle
1
Вы уверены, что это работает как надо? вы хэшируете метку времени с сообщением и кэшируете это сообщение. Это будет означать разные подписи каждого запроса, что сделает вашу кэшированную подпись бесполезной.
Филип Стас
1
@FilipStas: кажется, я не понимаю вашу точку зрения, причина использовать Cache здесь, чтобы предотвратить релейную атаку, ничего более
cuongle
1
@ChrisO: Вы можете ссылаться [на эту страницу] ( jokecamp.wordpress.com/2012/10/21/… ). Я скоро
обновлю
1
Предложенное решение работает, но вы не можете предотвратить атаку «Человек посередине», для этого вам нужно реализовать HTTPS
рефакторинг
34

Я бы посоветовал начать с самых простых решений - возможно, в вашем сценарии достаточно простой базовой аутентификации HTTP + HTTPS.

Если нет (например, вы не можете использовать https или нуждаетесь в более сложном управлении ключами), вы можете взглянуть на решения на основе HMAC, предложенные другими. Хорошим примером такого API был бы Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

Я написал сообщение в блоге об аутентификации на основе HMAC в ASP.NET Web API. В нем рассматриваются как служба веб-API, так и клиент веб-API, а код доступен на bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Вот пост о базовой аутентификации в веб-API: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

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

PS. Также есть возможность использовать HTTPS + сертификаты. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

Петр Уолат
источник
23

Вы пробовали DevDefined.OAuth?

Я использовал его для защиты своего WebApi с 2-Legged OAuth. Я также успешно проверил это с клиентами PHP.

С помощью этой библиотеки довольно легко добавить поддержку OAuth. Вот как вы можете реализовать провайдера для ASP.NET MVC Web API:

1) Получите исходный код DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - новейшая версия обеспечивает OAuthContextBuilderрасширяемость.

2) Создайте библиотеку и сделайте ссылку на нее в своем проекте Web API.

3) Создайте пользовательский конструктор контекста для поддержки построения контекста из HttpRequestMessage:

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Net.Http;
using System.Web;

using DevDefined.OAuth.Framework;

public class WebApiOAuthContextBuilder : OAuthContextBuilder
{
    public WebApiOAuthContextBuilder()
        : base(UriAdjuster)
    {
    }

    public IOAuthContext FromHttpRequest(HttpRequestMessage request)
    {
        var context = new OAuthContext
            {
                RawUri = this.CleanUri(request.RequestUri), 
                Cookies = this.CollectCookies(request), 
                Headers = ExtractHeaders(request), 
                RequestMethod = request.Method.ToString(), 
                QueryParameters = request.GetQueryNameValuePairs()
                    .ToNameValueCollection(), 
            };

        if (request.Content != null)
        {
            var contentResult = request.Content.ReadAsByteArrayAsync();
            context.RawContent = contentResult.Result;

            try
            {
                // the following line can result in a NullReferenceException
                var contentType = 
                    request.Content.Headers.ContentType.MediaType;
                context.RawContentType = contentType;

                if (contentType.ToLower()
                    .Contains("application/x-www-form-urlencoded"))
                {
                    var stringContentResult = request.Content
                        .ReadAsStringAsync();
                    context.FormEncodedParameters = 
                        HttpUtility.ParseQueryString(stringContentResult.Result);
                }
            }
            catch (NullReferenceException)
            {
            }
        }

        this.ParseAuthorizationHeader(context.Headers, context);

        return context;
    }

    protected static NameValueCollection ExtractHeaders(
        HttpRequestMessage request)
    {
        var result = new NameValueCollection();

        foreach (var header in request.Headers)
        {
            var values = header.Value.ToArray();
            var value = string.Empty;

            if (values.Length > 0)
            {
                value = values[0];
            }

            result.Add(header.Key, value);
        }

        return result;
    }

    protected NameValueCollection CollectCookies(
        HttpRequestMessage request)
    {
        IEnumerable<string> values;

        if (!request.Headers.TryGetValues("Set-Cookie", out values))
        {
            return new NameValueCollection();
        }

        var header = values.FirstOrDefault();

        return this.CollectCookiesFromHeaderString(header);
    }

    /// <summary>
    /// Adjust the URI to match the RFC specification (no query string!!).
    /// </summary>
    /// <param name="uri">
    /// The original URI. 
    /// </param>
    /// <returns>
    /// The adjusted URI. 
    /// </returns>
    private static Uri UriAdjuster(Uri uri)
    {
        return
            new Uri(
                string.Format(
                    "{0}://{1}{2}{3}", 
                    uri.Scheme, 
                    uri.Host, 
                    uri.IsDefaultPort ?
                        string.Empty :
                        string.Format(":{0}", uri.Port), 
                    uri.AbsolutePath));
    }
}

4) Используйте это руководство для создания поставщика OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . На последнем шаге (Пример доступа к защищенным ресурсам) вы можете использовать этот код в своем AuthorizationFilterAttributeатрибуте:

public override void OnAuthorization(HttpActionContext actionContext)
{
    // the only change I made is use the custom context builder from step 3:
    OAuthContext context = 
        new WebApiOAuthContextBuilder().FromHttpRequest(actionContext.Request);

    try
    {
        provider.AccessProtectedResourceRequest(context);

        // do nothing here
    }
    catch (OAuthException authEx)
    {
        // the OAuthException's Report property is of the type "OAuthProblemReport", it's ToString()
        // implementation is overloaded to return a problem report string as per
        // the error reporting OAuth extension: http://wiki.oauth.net/ProblemReporting
        actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized)
            {
               RequestMessage = request, ReasonPhrase = authEx.Report.ToString()
            };
    }
}

Я реализовал свой собственный провайдер, поэтому я не тестировал приведенный выше код (кроме, конечно, того, WebApiOAuthContextBuilderкоторый я использую в своем провайдере), но он должен работать нормально.

Максимилиан Мажер
источник
Спасибо - я посмотрю на это, хотя сейчас я развернул свое собственное решение на основе HMAC.
Крейг Ширер
1
@CraigShearer - привет, вы говорите, что катали свои собственные ... просто было несколько вопросов, если вы не против поделиться. Я нахожусь в аналогичной должности, где у меня относительно небольшой веб-API MVC. Контроллеры API находятся рядом с другими контроллерами / действиями, которые находятся под формами auth. Внедрение OAuth кажется излишним, когда у меня уже есть провайдер членства, которым я мог бы пользоваться, и мне нужно только обеспечить несколько операций. Я действительно хочу действие авторизации, которое возвращает зашифрованный токен - затем использовал токен в последующих вызовах? Любая информация приветствуется, прежде чем я обязуюсь реализовать существующее решение для аутентификации. Спасибо!
Самбомартен
@Maksymilian Majer - Можете ли вы рассказать подробнее о том, как вы реализовали провайдера? У меня возникли проблемы с отправкой ответов клиенту.
Jlrolin
21

Веб-API представил атрибут [Authorize]для обеспечения безопасности. Это может быть установлено глобально (global.asx)

public static void Register(HttpConfiguration config)
{
    config.Filters.Add(new AuthorizeAttribute());
}

Или на контроллер:

[Authorize]
public class ValuesController : ApiController{
...

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

public class DemoAuthorizeAttribute : AuthorizeAttribute
{
    public override void OnAuthorization(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        if (Authorize(actionContext))
        {
            return;
        }
        HandleUnauthorizedRequest(actionContext);
    }

    protected override void HandleUnauthorizedRequest(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        var challengeMessage = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        challengeMessage.Headers.Add("WWW-Authenticate", "Basic");
        throw new HttpResponseException(challengeMessage);
    }

    private bool Authorize(System.Web.Http.Controllers.HttpActionContext actionContext)
    {
        try
        {
            var someCode = (from h in actionContext.Request.Headers where h.Key == "demo" select h.Value.First()).FirstOrDefault();
            return someCode == "myCode";
        }
        catch (Exception)
        {
            return false;
        }
    }
}

И в вашем контроллере:

[DemoAuthorize]
public class ValuesController : ApiController{

Вот ссылка на другую пользовательскую реализацию для авторизаций WebApi:

http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-membership-provider/

Dalorzo
источник
Спасибо за пример @Dalorzo, но у меня есть некоторые проблемы. Я посмотрел на приложенную ссылку, но следуя этой инструкции не совсем работает. Я также обнаружил, что нужная информация отсутствует. Во-первых, когда я создаю новый проект, правильно ли выбирать индивидуальные учетные записи пользователей для аутентификации? Или я оставлю это без аутентификации. Я также не получаю упомянутую ошибку 302, но получаю ошибку 401. Наконец, как я могу передать необходимую информацию из моего представления в контроллер? Как должен выглядеть мой вызов ajax? Кстати, я использую аутентификацию форм для моих представлений MVC. Это проблема?
Аманда
Это работает фантастически. Просто приятно учиться и начинать работать над собственными токенами доступа.
CodeName47
Один небольшой комментарий - будьте осторожны AuthorizeAttribute, так как в разных пространствах имен есть два разных класса с одинаковыми именами: 1. System.Web.Mvc.AuthorizeAttribute -> для контроллеров MVC 2. System.Web.Http.AuthorizeAttribute -> для WebApi.
Виталий Маркитанов
5

Если вы хотите защитить свой API-интерфейс в режиме «сервер-сервер» (нет переадресации на веб-сайт для двухсторонней аутентификации). Вы можете посмотреть протокол OAuth2 Client Credentials Grant.

https://dev.twitter.com/docs/auth/application-only-auth

Я разработал библиотеку, которая поможет вам легко добавить такую ​​поддержку в ваш WebAPI. Вы можете установить его как пакет NuGet:

https://nuget.org/packages/OAuth2ClientCredentialsGrant/1.0.0.0

Библиотека предназначена для .NET Framework 4.5.

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

Ура!

Варун Чаттерджи
источник
5
Вы разделяете / предоставляете исходный код для этой платформы как открытый исходный код?
barrypicker
JFR: Первая ссылка
повреждена,
3

в продолжение ответа @ Cuong Le, мой подход к предотвращению повторной атаки будет

// Зашифруем время Unix на стороне клиента с помощью общего закрытого ключа (или пароля пользователя)

// Отправить его как часть заголовка запроса на сервер (WEB API)

// Расшифровываем Unix Time at Server (WEB API), используя общий закрытый ключ (или пароль пользователя)

// Проверяем разницу во времени между Unix Time клиента и Unix Time сервера, не должна превышать x sec

// если идентификатор пользователя / хэш-пароль верны и дешифрованный UnixTime находится в пределах x секунд времени сервера, то это допустимый запрос

рефакторинг
источник