So sichern Sie eine ASP.NET-Web-API [geschlossen]

397

Ich möchte mithilfe der ASP.NET-Web-API einen RESTful- Webdienst erstellen, mit dem Entwickler von Drittanbietern auf die Daten meiner Anwendung zugreifen.

Ich habe ziemlich viel über OAuth gelesen und es scheint der Standard zu sein, aber ein gutes Beispiel mit einer Dokumentation zu finden, die erklärt, wie es funktioniert (und das funktioniert tatsächlich!), Scheint unglaublich schwierig zu sein (insbesondere für einen Neuling in OAuth).

Gibt es ein Beispiel, das tatsächlich erstellt und funktioniert und zeigt, wie dies implementiert wird?

Ich habe zahlreiche Beispiele heruntergeladen:

  • DotNetOAuth - Dokumentation ist aus der Perspektive eines Neulings hoffnungslos
  • Thinktecture - kann es nicht zum Bauen bringen

Ich habe mir auch Blogs angesehen, in denen ein einfaches Token-basiertes Schema (wie dieses ) vorgeschlagen wurde. Dies scheint eine Neuerfindung des Rads zu sein, hat jedoch den Vorteil, dass es konzeptionell ziemlich einfach ist.

Es scheint, dass es auf SO viele Fragen wie diese gibt, aber keine guten Antworten.

Was machen alle in diesem Raum?

Craig Shearer
quelle

Antworten:

292

Aktualisieren:

Ich habe diesen Link zu meiner anderen Antwort hinzugefügt, wie die JWT-Authentifizierung für die ASP.NET-Web-API hier für alle verwendet wird, die sich für JWT interessieren.


Wir haben es geschafft, die HMAC-Authentifizierung auf die sichere Web-API anzuwenden, und es hat einwandfrei funktioniert. Bei der HMAC-Authentifizierung wird für jeden Verbraucher ein geheimer Schlüssel verwendet, von dem sowohl der Verbraucher als auch der Server wissen, dass er eine Nachricht hasht. HMAC256 sollte verwendet werden. In den meisten Fällen wird das Hash-Passwort des Verbrauchers als geheimer Schlüssel verwendet.

Die Nachricht besteht normalerweise aus Daten in der HTTP-Anforderung oder sogar aus benutzerdefinierten Daten, die dem HTTP-Header hinzugefügt werden. Die Nachricht kann Folgendes enthalten:

  1. Zeitstempel: Zeit, zu der die Anforderung gesendet wird (UTC oder GMT)
  2. HTTP-Verb: GET, POST, PUT, DELETE.
  3. Postdaten und Abfragezeichenfolge,
  4. URL

Unter der Haube wäre die HMAC-Authentifizierung:

Der Verbraucher sendet eine HTTP-Anfrage an den Webserver, nachdem er die Signatur (Ausgabe des hmac-Hash), die Vorlage der HTTP-Anfrage, erstellt hat:

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

Beispiel für eine GET-Anfrage:

GET /webapi.hmac/api/values

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

Die Nachricht an Hash, um die Signatur zu erhalten:

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

Beispiel für eine POST-Anfrage mit Abfragezeichenfolge (Signatur unten ist nicht korrekt, nur ein Beispiel)

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

Die Nachricht an Hash, um die Signatur zu erhalten

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

Bitte beachten Sie, dass Formulardaten und Abfragezeichenfolge in der richtigen Reihenfolge sein sollten, damit der Code auf dem Server Abfragezeichenfolgen und Formulardaten erhält, um die richtige Nachricht zu erstellen.

Wenn eine HTTP-Anforderung an den Server gesendet wird, wird ein Authentifizierungsaktionsfilter implementiert, um die Anforderung zum Abrufen von Informationen zu analysieren: HTTP-Verb, Zeitstempel, URL, Formulardaten und Abfragezeichenfolge, basierend auf diesen, um eine Signatur (Verwendung von hmac-Hash) mit dem Geheimnis zu erstellen Schlüssel (Hash-Passwort) auf dem Server.

Der geheime Schlüssel wird aus der Datenbank mit dem Benutzernamen auf der Anfrage abgerufen.

Anschließend vergleicht der Servercode die Signatur auf der Anforderung mit der erstellten Signatur. Wenn gleich, wird die Authentifizierung übergeben, andernfalls ist sie fehlgeschlagen.

Der Code zum Erstellen der Signatur:

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

Wie kann man einen Wiederholungsangriff verhindern?

Fügen Sie eine Einschränkung für den Zeitstempel hinzu, z. B.:

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

(Servertime: Zeitpunkt der Anforderung an den Server)

Und zwischenspeichern Sie die Signatur der Anforderung im Speicher (verwenden Sie MemoryCache, sollte im Zeitlimit bleiben). Wenn die nächste Anfrage dieselbe Signatur wie die vorherige Anfrage hat, wird sie abgelehnt.

Der Demo-Code lautet wie folgt: https://github.com/cuongle/Hmac.WebApi

cuongle
quelle
2
@James: Nur Zeitstempel scheint nicht viel zu sein, in kurzer Zeit können sie die Anfrage simulieren und an den Server senden. Ich habe gerade meinen Beitrag bearbeitet, beides wäre am besten.
Cuongle
1
Sind Sie sicher, dass dies so funktioniert, wie es sollte? Sie haben den Zeitstempel mit der Nachricht gehasht und diese Nachricht zwischengespeichert. Dies würde für jede Anforderung eine andere Signatur bedeuten, wodurch Ihre zwischengespeicherte Signatur unbrauchbar würde.
Filip Stas
1
@FilipStas: Anscheinend verstehe ich Ihren Standpunkt nicht. Der Grund, Cache hier zu verwenden, besteht darin, einen Relaisangriff zu verhindern, nichts weiter
cuongle
1
@ChrisO: Sie können auf [diese Seite] verweisen ( Witzcamp.wordpress.com/2012/10/21/… ). Ich werde diese Quelle bald aktualisieren
cuongle
1
Die vorgeschlagene Lösung funktioniert, aber Sie können einen Man-in-the-Middle-Angriff nicht verhindern. Dazu müssen Sie HTTPS implementieren
Refactor
34

Ich würde vorschlagen, zuerst mit den einfachsten Lösungen zu beginnen - vielleicht reicht in Ihrem Szenario eine einfache HTTP-Basisauthentifizierung + HTTPS aus.

Wenn nicht (zum Beispiel können Sie https nicht verwenden oder benötigen eine komplexere Schlüsselverwaltung), können Sie sich HMAC-basierte Lösungen ansehen, wie von anderen vorgeschlagen. Ein gutes Beispiel für eine solche API wäre Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html ).

Ich habe einen Blogbeitrag über die HMAC-basierte Authentifizierung in der ASP.NET-Web-API geschrieben. Es werden sowohl der Web-API-Dienst als auch der Web-API-Client erläutert, und der Code ist auf bitbucket verfügbar. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Hier ist ein Beitrag über die Standardauthentifizierung in der Web-API: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

Denken Sie daran, dass Sie höchstwahrscheinlich auch für die Bereitstellung von Client-Bibliotheken verantwortlich sind, wenn Sie eine API für Dritte bereitstellen. Die Standardauthentifizierung hat hier einen erheblichen Vorteil, da sie auf den meisten sofort einsatzbereiten Programmierplattformen unterstützt wird. HMAC hingegen ist nicht so standardisiert und erfordert eine benutzerdefinierte Implementierung. Diese sollten relativ einfach sein, aber dennoch Arbeit erfordern.

PS. Es besteht auch die Möglichkeit, HTTPS + -Zertifikate zu verwenden. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

Piotr Walat
quelle
23

Haben Sie DevDefined.OAuth ausprobiert?

Ich habe es verwendet, um mein WebApi mit 2-Legged OAuth zu sichern. Ich habe es auch erfolgreich mit PHP-Clients getestet.

Mit dieser Bibliothek können Sie ganz einfach Unterstützung für OAuth hinzufügen. So können Sie den Anbieter für die ASP.NET MVC-Web-API implementieren:

1) Holen Sie sich den Quellcode von DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - die neueste Version ermöglicht OAuthContextBuilderErweiterbarkeit.

2) Erstellen Sie die Bibliothek und verweisen Sie in Ihrem Web-API-Projekt darauf.

3) Erstellen Sie einen benutzerdefinierten Kontext-Builder, um das Erstellen eines Kontexts aus folgenden Funktionen zu unterstützen 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) Verwenden Sie dieses Tutorial zum Erstellen eines OAuth-Anbieters: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . Im letzten Schritt (Beispiel für den Zugriff auf geschützte Ressourcen) können Sie diesen Code in Ihrem AuthorizationFilterAttributeAttribut verwenden:

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

Ich habe meinen eigenen Provider implementiert, daher habe ich den obigen Code nicht getestet (außer natürlich den, WebApiOAuthContextBuilderden ich in meinem Provider verwende), aber er sollte gut funktionieren.

Maksymilian Majer
quelle
Danke - ich werde mir das ansehen, obwohl ich jetzt meine eigene HMAC-basierte Lösung entwickelt habe.
Craig Shearer
1
@CraigShearer - Hallo, du sagst, du hast deine eigenen gerollt. Ich hatte nur ein paar Fragen, wenn es dir nichts ausmacht, sie zu teilen. Ich bin in einer ähnlichen Position, wo ich eine relativ kleine MVC-Web-API habe. Die API-Controller befinden sich neben anderen Controllern / Aktionen, die sich unter Formularauthentifizierung befinden. Die Implementierung von OAuth scheint ein Overkill zu sein, wenn ich bereits einen Mitgliedschaftsanbieter habe, den ich verwenden kann, und ich nur eine Handvoll Operationen sichern muss. Ich möchte wirklich eine Authentifizierungsaktion, die ein verschlüsseltes Token zurückgibt und das Token dann in nachfolgenden Aufrufen verwendet. Alle Informationen sind willkommen, bevor ich mich zur Implementierung einer vorhandenen Authentifizierungslösung verpflichte. Vielen Dank!
Sambomartin
@Maksymilian Majer - Gibt es eine Chance, Ihnen mitzuteilen, wie Sie den Anbieter detaillierter implementiert haben? Ich habe einige Probleme beim Senden von Antworten an den Client.
Jlrolin
21

Die Web-API hat ein Attribut eingeführt [Authorize], um die Sicherheit zu gewährleisten. Dies kann global eingestellt werden (global.asx)

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

Oder pro Controller:

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

Natürlich kann Ihre Art der Authentifizierung variieren und Sie möchten möglicherweise Ihre eigene Authentifizierung durchführen. In diesem Fall kann es hilfreich sein, von Authorizate Attribute zu erben und es zu erweitern, um Ihren Anforderungen zu entsprechen:

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

Und in Ihrem Controller:

[DemoAuthorize]
public class ValuesController : ApiController{

Hier ist ein Link zu anderen benutzerdefinierten Implementierungen für WebApi-Berechtigungen:

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

Dalorzo
quelle
Danke für das Beispiel @Dalorzo, aber ich habe einige Probleme. Ich habe mir den angehängten Link angesehen, aber das Befolgen dieser Anweisungen funktioniert nicht ganz. Ich fand auch benötigte Informationen fehlen. Ist es beim Erstellen des neuen Projekts richtig, einzelne Benutzerkonten für die Authentifizierung auszuwählen? Oder lasse ich es bei keiner Authentifizierung. Ich erhalte auch nicht den erwähnten 302-Fehler, aber einen 401-Fehler. Wie gebe ich die benötigten Informationen aus meiner Sicht an den Controller weiter? Wie muss mein Ajax-Anruf aussehen? Übrigens verwende ich die Formularauthentifizierung für meine MVC-Ansichten. Ist das ein Problem?
Amanda
Es funktioniert fantastisch. Einfach schön zu lernen und an unseren eigenen Zugriffstoken zu arbeiten.
CodeName47
Ein kleiner Kommentar - seien Sie vorsichtig AuthorizeAttribute, da es zwei verschiedene Klassen mit demselben Namen in verschiedenen Namespaces gibt: 1. System.Web.Mvc.AuthorizeAttribute -> für MVC-Controller 2. System.Web.Http.AuthorizeAttribute -> für WebApi.
Vitaliy Markitanov
5

Wenn Sie Ihre API von Server zu Server sichern möchten (keine Umleitung zur Website für die zweibeinige Authentifizierung). Sie können sich das OAuth2 Client Credentials Grant-Protokoll ansehen.

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

Ich habe eine Bibliothek entwickelt, mit der Sie diese Art von Unterstützung ganz einfach zu Ihrer WebAPI hinzufügen können. Sie können es als NuGet-Paket installieren:

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

Die Bibliothek zielt auf .NET Framework 4.5 ab.

Sobald Sie das Paket zu Ihrem Projekt hinzugefügt haben, wird eine Readme-Datei im Stammverzeichnis Ihres Projekts erstellt. In dieser Readme-Datei können Sie sehen, wie Sie dieses Paket konfigurieren / verwenden.

Prost!

Varun Chatterji
quelle
5
Teilen Sie Quellcode für dieses Framework als Open Source?
Barrypicker
JFR: Der erste Link ist defekt und das NuGet-Paket wurde nie aktualisiert
abdul qayyum
3

In Fortsetzung der Antwort von @ Cuong Le wäre mein Ansatz, einen Wiederholungsangriff zu verhindern

// Verschlüsseln Sie die Unix-Zeit auf Clientseite mit dem freigegebenen privaten Schlüssel (oder dem Kennwort des Benutzers).

// Sende es als Teil des Anforderungsheaders an den Server (WEB API)

// Entschlüsseln Sie die Unix-Zeit am Server (WEB-API) mit dem freigegebenen privaten Schlüssel (oder dem Kennwort des Benutzers).

// Überprüfen Sie den Zeitunterschied zwischen der Unix-Zeit des Clients und der Unix-Zeit des Servers. Er sollte nicht größer als x Sekunden sein

// Wenn Benutzer-ID / Hash-Passwort korrekt sind und die entschlüsselte UnixTime innerhalb von x Sekunden der Serverzeit liegt, handelt es sich um eine gültige Anforderung

Refactor
quelle