Jak zabezpieczyć API sieci Web ASP.NET [zamknięte]

397

Chcę zbudować usługę sieci Web RESTful przy użyciu interfejsu API sieci Web ASP.NET, z którego programiści zewnętrzni będą uzyskiwać dostęp do danych mojej aplikacji.

Dużo czytałem o OAuth i wydaje się, że jest to standard, ale znalezienie dobrej próbki z dokumentacją wyjaśniającą, jak to działa (i to faktycznie działa!) Wydaje się niezwykle trudne (szczególnie dla początkującego OAuth).

Czy istnieje próbka, która faktycznie buduje, działa i pokazuje, jak to zaimplementować?

Pobrałem wiele próbek:

  • DotNetOAuth - dokumentacja jest beznadziejna z perspektywy początkującego
  • Thinktecture - nie można go zbudować

Patrzyłem również na blogi sugerujące prosty schemat oparty na tokenach (taki jak ten ) - wygląda to na ponowne wynalezienie koła, ale ma tę zaletę, że jest koncepcyjnie dość proste.

Wygląda na to, że na SO jest wiele takich pytań, ale nie ma dobrych odpowiedzi.

Co wszyscy robią w tej przestrzeni?

Craig Shearer
źródło

Odpowiedzi:

292

Aktualizacja:

Dodałem ten link do mojej innej odpowiedzi, jak używać uwierzytelniania JWT dla API sieci Web ASP.NET tutaj dla wszystkich zainteresowanych JWT.


Udało nam się zastosować uwierzytelnianie HMAC do bezpiecznego interfejsu API sieci Web i działało dobrze. Uwierzytelnianie HMAC używa tajnego klucza dla każdego konsumenta, który zarówno klient, jak i serwer wiedzą, że hmac hash wiadomość, należy użyć HMAC256. W większości przypadków zaszyfrowane hasło konsumenta jest używane jako tajny klucz.

Wiadomość zwykle jest zbudowana z danych w żądaniu HTTP lub nawet niestandardowych danych, które są dodawane do nagłówka HTTP, wiadomość może zawierać:

  1. Znacznik czasu: godzina wysłania żądania (UTC lub GMT)
  2. Czasownik HTTP: GET, POST, PUT, DELETE.
  3. przesłać dane i ciąg zapytania,
  4. URL

Pod maską uwierzytelnianie HMAC byłoby:

Konsument wysyła żądanie HTTP do serwera WWW, po zbudowaniu podpisu (dane wyjściowe skrótu hmac) szablon żądania HTTP:

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

Przykład żądania GET:

GET /webapi.hmac/api/values

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

Wiadomość do skrótu, aby uzyskać podpis:

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

Przykład żądania POST z ciągiem zapytania (poniższy podpis jest niepoprawny, tylko przykład)

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

Wiadomość do skrótu, aby uzyskać podpis

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

Należy pamiętać, że dane formularza i ciąg zapytania powinny być w porządku, więc kod na serwerze otrzymuje ciąg zapytania i dane formularza, aby zbudować poprawny komunikat.

Gdy żądanie HTTP przychodzi do serwera, wdrażany jest filtr akcji uwierzytelniania, który analizuje żądanie w celu uzyskania informacji: czasownik HTTP, znacznik czasu, identyfikator użytkownika, dane formularza i ciąg zapytania, a następnie na podstawie tych danych do zbudowania podpisu (użyj skrótu hmac) za pomocą klucza tajnego klucz (hashowane hasło) na serwerze.

Tajny klucz jest pobierany z bazy danych z nazwą użytkownika na żądanie.

Następnie kod serwera porównuje podpis na żądanie z podpisem zbudowanym; jeśli jest równy, uwierzytelnienie jest przekazywane, w przeciwnym razie nie powiedzie się.

Kod do budowy podpisu:

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;
}

Jak więc zapobiec atakowi powtórek?

Dodaj ograniczenie znacznika czasu, na przykład:

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

(servertime: czas nadejścia żądania do serwera)

I buforuj podpis żądania w pamięci (użyj MemoryCache, należy zachować limit czasu). Jeśli następne żądanie zostanie opatrzone tym samym podpisem co poprzednie, zostanie odrzucone.

Kod demonstracyjny jest umieszczony tutaj: https://github.com/cuongle/Hmac.WebApi

cuongle
źródło
2
@James: tylko znacznik czasu wydaje się niewystarczający, w krótkim czasie mogą zasymulować żądanie i wysłane do serwera, właśnie edytowałem swój post, użycie obu byłoby najlepsze.
cuongle,
1
Czy na pewno działa to tak, jak powinno? haszujesz sygnaturę czasową wiadomością i buforujesz tę wiadomość. Oznaczałoby to inny podpis dla każdego żądania, co sprawiłoby, że podpis w pamięci podręcznej byłby bezużyteczny.
Filip Stas,
1
@FilipStas: wygląda na to, że nie rozumiem o co chodzi, dlatego powodem użycia pamięci podręcznej jest zapobieganie atakowi sztafetowemu, nic więcej
cuongle
1
@ChrisO: Możesz odnieść się do [tej strony] ( jokecamp.wordpress.com/2012/10/21/... ). Wkrótce zaktualizuję to źródło
cuongle,
1
Sugerowane rozwiązanie działa, ale nie można zapobiec atakowi Man-in-the-Middle, w tym celu należy wdrożyć HTTPS
refaktoryzacja
34

Proponuję zacząć od najprostszych rozwiązań - może wystarczy proste uwierzytelnianie podstawowe HTTP + HTTPS w twoim scenariuszu.

Jeśli nie (na przykład nie możesz używać protokołu https lub potrzebujesz bardziej złożonego zarządzania kluczami), możesz spojrzeć na rozwiązania oparte na HMAC, jak sugerują inni. Dobrym przykładem takiego interfejsu API jest Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

Napisałem post na blogu o uwierzytelnianiu opartym na HMAC w ASP.NET Web API. Omówiono zarówno usługę Web API, jak i klienta Web API, a kod jest dostępny na bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Oto post dotyczący podstawowego uwierzytelniania w interfejsie API sieci Web: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

Pamiętaj, że jeśli zamierzasz udostępniać interfejs API stronom trzecim, najprawdopodobniej będziesz również odpowiedzialny za dostarczanie bibliotek klienckich. Uwierzytelnianie podstawowe ma tutaj znaczącą zaletę, ponieważ jest obsługiwane na większości platform programistycznych od razu po wyjęciu z pudełka. Z drugiej strony HMAC nie jest tak ustandaryzowany i będzie wymagał niestandardowej implementacji. Powinny być stosunkowo proste, ale nadal wymagają pracy.

PS. Istnieje również opcja użycia certyfikatów HTTPS +. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

Piotr Walat
źródło
23

Czy próbowałeś DevDefined.OAuth?

Użyłem go, aby zabezpieczyć moją WebApi za pomocą 2-Legged OAuth. Z powodzeniem przetestowałem to również z klientami PHP.

Korzystanie z tej biblioteki jest bardzo łatwe, aby dodać obsługę protokołu OAuth. Oto, w jaki sposób można wdrożyć dostawcę interfejsu API sieci Web ASP.NET MVC:

1) Pobierz kod źródłowy DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - najnowsza wersja pozwala na OAuthContextBuilderrozszerzalność.

2) Zbuduj bibliotekę i odwołaj się do niej w swoim projekcie Web API.

3) Utwórz niestandardowy konstruktor kontekstów, aby wspierać budowanie kontekstu z 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) Skorzystaj z tego samouczka, aby utworzyć dostawcę OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . W ostatnim kroku (Przykład dostępu do chronionych zasobów) możesz użyć tego kodu w swoim AuthorizationFilterAttributeatrybucie:

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()
            };
    }
}

Wdrożyłem własnego dostawcę, więc nie przetestowałem powyższego kodu (oprócz oczywiście tego, WebApiOAuthContextBuilderktórego używam u mojego dostawcy), ale powinien on działać dobrze.

Maksymilian Majer
źródło
Dzięki - przyjrzę się temu, choć na razie stworzyłem własne rozwiązanie oparte na HMAC.
Craig Shearer
1
@CraigShearer - cześć, mówisz, że wyrzuciłeś własne ... po prostu masz kilka pytań, jeśli nie masz nic przeciwko udostępnianiu. Jestem w podobnej sytuacji, gdzie mam stosunkowo mały interfejs API sieci MVC. Kontrolery API siedzą obok innych kontrolerów / akcji, które są w ramach uwierzytelniania formularzy. Wdrażanie protokołu OAuth wydaje się przesadą, gdy mam już dostawcę członkostwa, z którego mogę korzystać i muszę jedynie zabezpieczyć garść operacji. Naprawdę chcę akcji uwierzytelnienia, która zwraca zaszyfrowany token - a następnie używał tokena w kolejnych wywołaniach? wszelkie informacje są mile widziane, zanim zdecyduję się na wdrożenie istniejącego rozwiązania uwierzytelniającego. dzięki!
sambomartin
@Maksymilian Majer - Czy jest jakaś szansa, abyś mógł bardziej szczegółowo opisać sposób wdrożenia dostawcy? Mam problemy z wysyłaniem odpowiedzi z powrotem do klienta.
jlrolin
21

Interfejs API sieci Web wprowadził atrybut [Authorize]zapewniający bezpieczeństwo. Można to ustawić globalnie (global.asx)

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

Lub na kontrolera:

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

Oczywiście twój typ uwierzytelnienia może się różnić i możesz chcieć przeprowadzić własne uwierzytelnianie, gdy to nastąpi, możesz znaleźć przydatne dziedziczenie po autoryzacji atrybutu i rozszerzenie go, aby spełnić twoje wymagania:

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;
        }
    }
}

A w twoim kontrolerze:

[DemoAuthorize]
public class ValuesController : ApiController{

Oto link do innej niestandardowej implementacji autoryzacji WebApi:

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

Dalorzo
źródło
Dzięki za przykład @Dalorzo, ale mam pewne problemy. Spojrzałem na załączony link, ale przestrzeganie tych instrukcji nie działa. Znalazłem też potrzebne informacje brakujące. Po pierwsze, kiedy tworzę nowy projekt, czy słuszne jest wybranie indywidualnych kont użytkowników do uwierzytelnienia? Czy pozostawiam to bez uwierzytelnienia. Nie otrzymuję również wspomnianego błędu 302, ale otrzymuję błąd 401. Wreszcie, jak przekazać potrzebne informacje z mojego widoku do kontrolera? Jak musi wyglądać moje wywołanie ajax? Btw, używam uwierzytelniania formularzy dla moich widoków MVC. Czy to problem?
Amanda,
Działa fantastycznie. Po prostu miło jest nauczyć się i zacząć pracować nad własnymi tokenami dostępu.
CodeName47,
Jeden mały komentarz - uważaj AuthorizeAttribute, ponieważ istnieją dwie różne klasy o tej samej nazwie, w różnych przestrzeniach nazw: 1. System.Web.Mvc.AuthorizeAttribute -> dla kontrolerów MVC 2. System.Web.Http.AuthorizeAttribute -> dla WebApi.
Vitaliy Markitanov,
5

Jeśli chcesz zabezpieczyć swój interfejs API w sposób zgodny z serwerem (bez przekierowania na stronę internetową w celu uwierzytelnienia 2-etapowego). Możesz spojrzeć na protokół OAuth2 Client Credentials Grant.

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

Opracowałem bibliotekę, która może pomóc w łatwym dodaniu tego rodzaju wsparcia do interfejsu WebAPI. Możesz zainstalować go jako pakiet NuGet:

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

Biblioteka jest przeznaczona dla .NET Framework 4.5.

Po dodaniu pakietu do projektu utworzy on plik readme w katalogu głównym projektu. Możesz spojrzeć na ten plik readme, aby zobaczyć, jak skonfigurować / używać tego pakietu.

Twoje zdrowie!

Varun Chatterji
źródło
5
Czy udostępniasz / udostępniasz kod źródłowy dla tego frameworka jako open source?
barrypicker
JFR: Pierwszy link jest zepsuty, a pakiet NuGet nigdy nie był aktualizowany
abdul qayyum
3

kontynuując odpowiedź @ Cuong Le, moje podejście do zapobiegania atakowi powtórek byłoby

// Szyfruj czas uniksowy po stronie klienta przy użyciu wspólnego klucza prywatnego (lub hasła użytkownika)

// Wyślij jako część nagłówka żądania na serwer (WEB API)

// Odszyfruj czas uniksowy na serwerze (WEB API) przy użyciu wspólnego klucza prywatnego (lub hasła użytkownika)

// Sprawdź różnicę czasu między czasem uniksowym klienta a czasem uniksowym serwera, nie powinna być większa niż x sek

// jeśli identyfikator użytkownika / hasło skrótu są poprawne, a odszyfrowany UnixTime znajduje się w odległości x sekund od czasu serwera, to jest to prawidłowe żądanie

refaktor
źródło