Як захистити веб-API ASP.NET [закрито]


397

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

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

Чи є зразок, який насправді будує та працює і показує, як це реалізувати?

Я завантажив численні зразки:

  • DotNetOAuth - документація безперспективна з точки зору новачка
  • Роздум - не в змозі побудувати його

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

Здається, є багато подібних запитань щодо ТА, але хороших відповідей немає.

Що всі роблять у цьому просторі?

Відповіді:


292

Оновлення:

Я додав це посилання до своєї іншої відповіді, як використовувати автентифікацію JWT для веб-API ASP.NET тут для всіх, хто цікавиться JWT.


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

Повідомлення зазвичай будується з даних запиту HTTP або навіть налаштованих даних, які додаються до заголовка HTTP, повідомлення може включати:

  1. Часова позначка: час надсилання запиту (UTC або GMT)
  2. HTTP дієслово: GET, POST, PUT, DELETE.
  3. розмістити дані та рядок запиту,
  4. URL-адреса

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

Споживач надсилає HTTP-запит на веб-сервер після складання підпису (вихід хмакового хеша) шаблону 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, даних форми та рядка запиту, а потім на їх основі будувати підпис (використовувати хмак хеш) із секретом ключ (хешований пароль) на сервері.

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

Потім код сервера порівнює підпис під запитом із вбудованим підписом; якщо рівне, автентифікація передається, інакше вона не вдалася.

Код для складання підпису:

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
@James: тільки часової позначки здається недостатньою, за короткий час вони можуть імітувати запит і надсилати на сервер, я щойно відредагував свою публікацію, використання обох було б найкраще.
cuongle

1
Ви впевнені, що це працює як слід? ви перемішуєте часову позначку з повідомленням і кешуєте це повідомлення. Це означатиме різний підпис кожного запиту, який зробить ваш кешований підпис марним.
Філіп Стас

1
@FilipStas: схоже, я не розумію, причина використання кешу тут - запобігання ретрансляції, більше нічого
cuongle

1
@ChrisO: Ви можете посилатися на [цю сторінку] ( jokecamp.wordpress.com/2012/10/21/… ). Я
оновлю

1
Пропоноване рішення працює, але ви не можете запобігти атаці "Людина в середині", тому що вам доведеться реалізувати HTTPS
refactor

34

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

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

Я написав повідомлення в блозі про аутентифікацію на основі HMAC у веб-API ASP.NET. У ньому обговорюються як сервіс Web API, так і клієнт Web 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/


23

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

Я використовував його для захисту свого WebApi за допомогою 2-ноги OAuth. Я також успішно протестував це з клієнтами PHP.

Додати підтримку OAuth за допомогою цієї бібліотеки досить просто. Ось як можна реалізувати провайдера для веб-API ASP.NET MVC:

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 сидять поруч з іншими контролерами / діями, які знаходяться під формами авт. Реалізація OAuth здається непосильним, коли у мене вже є постачальник членства, який я міг би використовувати, і мені потрібно лише забезпечити кілька операцій. Мені дуже хочеться автентичної дії, яка повертає зашифрований маркер - потім використовував маркер у наступних дзвінках? будь-яка інформація, ласкаво просимо до того, як я покладуся на реалізацію існуючого рішення для аутентифікації. Дякую!
sambomartin

@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, але у мене є деякі проблеми. Я переглянув додане посилання, але слідувати цій інструкції не дуже добре. Я також виявив, що потрібна інформація відсутня. По-перше, коли я створюю новий проект, чи правильно обирати індивідуальні облікові записи користувачів для аутентифікації? Або я залишаю його без автентифікації. Я також не отримую згадану помилку 302, але отримую помилку 401. Нарешті, як я можу передавати потрібну інформацію з мого перегляду контролеру? Як повинен виглядати мій дзвінок Ajax? До речі, я використовую автентифікацію форм для моїх переглядів MVC. Це проблема?
Аманда

Це працює фантастично. Просто приємно вчитися і починати працювати над власними жетонами доступу.
CodeName47

Один невеликий коментар - будьте обережні AuthorizeAttribute, оскільки у різних просторах імен є два різних класи з одним іменем: 1. System.Web.Mvc.AuthorizeAttribute -> для контролерів MVC 2. System.Web.Http.AuthorizeAttribute -> для WebApi.
Віталій Маркітанов

5

Якщо ви хочете захистити свій API в режимі сервера до сервера (немає перенаправлення на веб-сайт для двосторонній аутентифікації). Ви можете подивитися протокол надання грантів для клієнтів 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, щоб побачити, як налаштувати / використовувати цей пакет.

Ура!


5
Ви ділитесь / надаєте вихідний код для цієї рамки як відкритий код?
barrypicker

JFR: Перше посилання зірвано, і пакет NuGet ніколи не оновлювався
abdul qayyum

3

продовжуючи відповідь @ Cuong Le, мій підхід до запобігання повторній атаці був би

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

// Надіслати його як частину заголовка запиту на сервер (WEB API)

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

// Перевірте різницю у часі між Unix-часом клієнта та Unix-часом сервера, яка не повинна перевищувати x сек

// якщо ідентифікатор користувача / пароль Hash правильні, а розшифрований UnixTime знаходиться протягом x сек серверного часу, то це дійсний запит

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.