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

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

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

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

Я загрузил многочисленные примеры:

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

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

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

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

Ответ 1

Обновить:

Я добавил эту ссылку в свой другой ответ, как использовать аутентификацию 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

Ответ 2

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

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

Я написал сообщение в блоге об аутентификации на основе HMAC в веб-интерфейсе ASP.NET. В нем рассматриваются как служба веб-API, так и клиент веб-API, и код доступен на битбакете. 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/

Ответ 3

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

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

Очень легко добавить поддержку OAuth, используя эту библиотеку. Здесь вы можете реализовать поставщика ASP.NET MVC Web API:

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

2) Создайте библиотеку и укажите ее в своем проекте веб-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. На последнем шаге (Accessing Protected Resource Example) вы можете использовать этот код в своем атрибуте 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 Report property is of the type "OAuthProblemReport", it 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, который я использую в своем провайдере), но он должен работать нормально.

Ответ 4

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/

Ответ 5

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

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, чтобы узнать, как настроить/использовать этот пакет.

Ура!

Ответ 6

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

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

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

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

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

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