Come proteggere un'API Web ASP.NET [chiusa]


397

Voglio creare un servizio Web RESTful utilizzando l'API Web ASP.NET che gli sviluppatori di terze parti utilizzeranno per accedere ai dati della mia applicazione.

Ho letto molto su OAuth e sembra essere lo standard, ma trovare un buon campione con la documentazione che spiega come funziona (e che effettivamente funziona!) Sembra essere incredibilmente difficile (specialmente per un principiante di OAuth).

Esiste un esempio che effettivamente costruisce e funziona e mostra come implementarlo?

Ho scaricato numerosi esempi:

  • DotNetOAuth: la documentazione è senza speranza da una prospettiva per principianti
  • Thinktecture - impossibile farlo costruire

Ho anche guardato ai blog che suggeriscono un semplice schema basato su token (come questo ) - questo sembra reinventare la ruota ma ha il vantaggio di essere concettualmente abbastanza semplice.

Sembra che ci siano molte domande come questa su SO ma nessuna buona risposta.

Cosa stanno facendo tutti in questo spazio?

Risposte:


292

Aggiornare:

Ho aggiunto questo link all'altra mia risposta su come utilizzare l'autenticazione JWT per l'API Web ASP.NET qui per chiunque sia interessato a JWT.


Siamo riusciti ad applicare l'autenticazione HMAC per proteggere l'API Web e ha funzionato bene. L'autenticazione HMAC utilizza una chiave segreta per ogni consumatore che sia il consumatore che il server sanno entrambi per che un hash hash un messaggio, HMAC256 dovrebbe essere usato. La maggior parte dei casi, la password con hash del consumatore viene utilizzata come chiave segreta.

Il messaggio viene normalmente generato dai dati nella richiesta HTTP o persino dai dati personalizzati che vengono aggiunti all'intestazione HTTP, il messaggio potrebbe includere:

  1. Data / ora: ora di invio della richiesta (UTC o GMT)
  2. Verbo HTTP: GET, POST, PUT, DELETE.
  3. inserisci dati e stringa di query,
  4. URL

Sotto il cofano, l'autenticazione HMAC sarebbe:

Il consumatore invia una richiesta HTTP al server web, dopo aver creato la firma (output di hmac hash), il modello di richiesta HTTP:

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

Esempio per la richiesta GET:

GET /webapi.hmac/api/values

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

Il messaggio all'hash per ottenere la firma:

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

Esempio di richiesta POST con stringa di query (la firma di seguito non è corretta, solo un esempio)

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

Il messaggio all'hash per ottenere la firma

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

Si noti che i dati del modulo e la stringa di query devono essere in ordine, quindi il codice sul server ottiene la stringa di query e i dati del modulo per creare il messaggio corretto.

Quando la richiesta HTTP arriva al server, viene implementato un filtro di azione di autenticazione per analizzare la richiesta per ottenere informazioni: verbo HTTP, timestamp, uri, dati del modulo e stringa di query, quindi basati su questi per costruire la firma (usa l'hash hmac) con il segreto chiave (password con hash) sul server.

La chiave segreta viene ottenuta dal database con il nome utente sulla richiesta.

Quindi il codice server confronta la firma sulla richiesta con la firma creata; se uguale, l'autenticazione viene passata, altrimenti non è riuscita.

Il codice per creare la firma:

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

Quindi, come prevenire l'attacco replay?

Aggiungi un vincolo per il timestamp, qualcosa del tipo:

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

(servertime: ora della richiesta in arrivo sul server)

E, memorizza nella cache la firma della richiesta (usa MemoryCache, dovrebbe rimanere nel limite di tempo). Se la richiesta successiva arriva con la stessa firma della richiesta precedente, verrà respinta.

Il codice demo è inserito come qui: https://github.com/cuongle/Hmac.WebApi


2
@James: solo la data e l'ora non sembrano sufficienti, in breve tempo possono simulare la richiesta e inviarla al server, ho appena modificato il mio post, utilizzare entrambi sarebbe il migliore.
cuongle

1
Sei sicuro che funzioni come dovrebbe? stai cancellando il timestamp con il messaggio e memorizzando nella cache quel messaggio. Ciò significherebbe una firma diversa per ogni richiesta che renderebbe inutile la firma memorizzata nella cache.
Filip Stas,

1
@FilipStas: sembra Non capisco il tuo punto, il motivo per utilizzare Cache qui è quello di impedire attacco di inoltro, niente di più
cuongle

1
@ChrisO: Puoi fare riferimento a [questa pagina] ( jokecamp.wordpress.com/2012/10/21/… ). Aggiornerò presto questa fonte
cuongle

1
La soluzione suggerita funziona, ma non puoi impedire l'attacco Man-in-the-Middle, per questo devi implementare HTTPS
refactor

34

Vorrei suggerire di iniziare con le soluzioni più semplici: forse nel tuo scenario è sufficiente la semplice autenticazione di base HTTP + HTTPS.

In caso contrario (ad esempio non è possibile utilizzare https o è necessaria una gestione delle chiavi più complessa), è possibile dare un'occhiata alle soluzioni basate su HMAC come suggerito da altri. Un buon esempio di tale API sarebbe Amazon S3 ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

Ho scritto un post sul blog sull'autenticazione basata su HMAC nell'API Web ASP.NET. Descrive sia il servizio API Web sia il client API Web e il codice è disponibile su bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Ecco un post sull'autenticazione di base nell'API Web: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

Ricorda che se hai intenzione di fornire un'API a terze parti, molto probabilmente sarai anche responsabile della consegna delle librerie client. L'autenticazione di base ha un vantaggio significativo in quanto è supportata dalla maggior parte delle piattaforme di programmazione. HMAC, d'altra parte, non è così standardizzato e richiederà un'implementazione personalizzata. Questi dovrebbero essere relativamente semplici ma richiedono comunque un lavoro.

PS. C'è anche un'opzione per usare i certificati HTTPS +. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/


23

Hai provato DevDefined.OAuth?

L'ho usato per proteggere il mio WebApi con OAuth a 2 zampe. Ho anche testato con successo con i client PHP.

È abbastanza facile aggiungere il supporto per OAuth usando questa libreria. Ecco come è possibile implementare il provider per l'API Web ASP.NET MVC:

1) Ottieni il codice sorgente di DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - la versione più recente consente di OAuthContextBuilderestensibilità.

2) Costruisci la libreria e fai riferimento nel tuo progetto API Web.

3) Creare un generatore di contesto personalizzato per supportare la creazione di un contesto da 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) Utilizzare questo tutorial per creare un provider OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . Nell'ultimo passaggio (Accesso all'esempio di risorsa protetta) è possibile utilizzare questo codice AuthorizationFilterAttributenell'attributo:

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

Ho implementato il mio provider quindi non ho testato il codice sopra (tranne ovviamente quello WebApiOAuthContextBuilderche sto usando nel mio provider) ma dovrebbe funzionare bene.


Grazie. Daremo un'occhiata a questo, anche se per ora ho lanciato la mia soluzione basata su HMAC.
Craig Shearer,

1
@CraigShearer - ciao, dici di aver fatto il tuo .. hai solo qualche domanda se non ti dispiace condividere. Sono in una posizione simile, dove ho un'API Web MVC relativamente piccola. I controller API si trovano accanto ad altri controller / azioni che sono sotto forma di autenticazione. L'implementazione di OAuth sembra eccessiva quando ho già un provider di appartenenza che potrei usare e ho solo bisogno di proteggere una manciata di operazioni. Voglio davvero un'azione di autenticazione che restituisca un token crittografato, quindi ha utilizzato il token nelle chiamate successive? qualsiasi informazione di benvenuto prima di impegnarmi a implementare una soluzione di autenticazione esistente. Grazie!
sambomartin,

@Maksymilian Majer - Qualche possibilità per condividere in modo dettagliato come hai implementato il provider? Sto riscontrando alcuni problemi con l'invio di risposte al client.
jlrolin,

21

L'API Web ha introdotto un attributo [Authorize]per fornire sicurezza. Questo può essere impostato a livello globale (global.asx)

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

O per controller:

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

Ovviamente il tuo tipo di autenticazione può variare e potresti voler eseguire la tua autenticazione, in questo caso potresti trovare utile ereditare dall'Attributo Autorizzato ed estenderlo per soddisfare i tuoi requisiti:

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

E nel tuo controller:

[DemoAuthorize]
public class ValuesController : ApiController{

Ecco un link su altre implementazioni personalizzate per le autorizzazioni WebApi:

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


Grazie per l'esempio @Dalorzo, ma ho qualche problema. Ho guardato il link allegato, ma seguendo queste istruzioni non funziona del tutto. Ho anche trovato mancanti le informazioni necessarie. Innanzitutto, quando creo il nuovo progetto, è giusto scegliere Account utente individuali per l'autenticazione? O lo lascio senza autenticazione. Inoltre non ricevo l'errore 302 menzionato, ma ricevo un errore 401. Infine, come posso passare le informazioni necessarie dalla mia vista al controller? Come deve essere la mia chiamata ajax? A proposito, sto usando l'autenticazione basata su moduli per le mie viste MVC. È un problema?
Amanda,

Funziona in modo fantastico. Semplicemente bello da imparare e iniziare a lavorare sui nostri token di accesso.
CodeName47

Un piccolo commento: fai attenzione AuthorizeAttribute, poiché esistono due classi diverse con lo stesso nome, in spazi dei nomi diversi: 1. System.Web.Mvc.AuthorizeAttribute -> per controller MVC 2. System.Web.Http.AuthorizeAttribute -> per WebApi.
Vitaliy Markitanov,

5

Se si desidera proteggere la propria API in un modo da server a server (nessun reindirizzamento al sito Web per l'autenticazione a 2 gambe). È possibile consultare il protocollo di concessione delle credenziali del client OAuth2.

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

Ho sviluppato una libreria che può aiutarti ad aggiungere facilmente questo tipo di supporto al tuo WebAPI. Puoi installarlo come pacchetto NuGet:

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

La libreria è destinata a .NET Framework 4.5.

Una volta aggiunto il pacchetto al progetto, verrà creato un file Leggimi nella radice del progetto. Puoi guardare quel file Leggimi per vedere come configurare / usare questo pacchetto.

Saluti!


5
Stai condividendo / fornendo il codice sorgente per questo framework come open source?
barrypicker

JFR: First Link è interrotto e il pacchetto NuGet non è mai stato aggiornato
abdul qayyum

3

in seguito alla risposta di @ Cuong Le, il mio approccio per prevenire l'attacco di replay sarebbe

// Crittografa il tempo Unix sul lato client utilizzando la chiave privata condivisa (o la password dell'utente)

// Invialo come parte dell'intestazione della richiesta al server (API WEB)

// Decifrare Unix Time at Server (API WEB) usando la chiave privata condivisa (o la password dell'utente)

// Controlla la differenza oraria tra il tempo Unix del cliente e il tempo Unix del server, non deve essere maggiore di x sec

// se l'ID utente / la password hash sono corretti e UnixTime decrittografato si trova entro x sec dall'ora del server, allora è una richiesta valida

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.