it-roy-ru.com

Как защитить веб-API ASP.NET

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

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

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

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

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

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

Кажется, на SO есть много подобных вопросов, но нет хороших ответов.

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

371
Craig Shearer

Update:

Я поставил еще один ответ, как использовать аутентификацию JWT для Web API здесь для всех, кто интересуется JWT:

Аутентификация JWT для Asp.Net Web Api


Нам удалось применить аутентификацию 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

278
cuongle

Я бы посоветовал начать с самых простых решений - возможно, в вашем сценарии достаточно простой базовой аутентификации 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/

30
Piotr Walat

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

22
Maksymilian Majer

Web 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/

20
Dalorzo

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

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

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

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

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

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

Ура!

5
Varun Chatterji

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

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

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

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

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

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

2
refactor