web-dev-qa-db-fra.com

Comment sécuriser une API Web ASP.NET

Je souhaite créer un service Web RESTful à l'aide de l'API Web ASP.NET que les développeurs tiers utiliseront pour accéder aux données de mon application.

J'ai beaucoup lu sur OAuth et cela semble être la norme, mais trouver un bon échantillon avec une documentation expliquant son fonctionnement (et qui en fait ça marche!) semble être incroyablement difficile (surtout pour un débutant chez OAuth).

Existe-t-il un exemple qui construit, fonctionne et montre comment implémenter cela?

J'ai téléchargé de nombreux exemples:

  • DotNetOAuth - la documentation est sans espoir du point de vue d'un débutant
  • Thinktecture - n'arrive pas à la construire

J'ai également consulté des blogs suggérant un schéma simple basé sur des jetons (comme this ) - cela semble être une réinvention de la roue, mais il a l'avantage d'être conceptuellement assez simple.

Il semble qu'il y ait beaucoup de questions comme celle-ci sur SO mais pas de bonnes réponses.

Qu'est-ce que tout le monde fait dans cet espace?

384
Craig Shearer

Mise à jour:

J'ai ajouté ce lien à mon autre réponse comment utiliser l'authentification JWT pour l'API Web ASP.NET ici, si vous êtes intéressé par JWT.


Nous avons réussi à appliquer l'authentification HMAC pour sécuriser l'API Web, et cela a fonctionné correctement. L'authentification HMAC utilise une clé secrète pour chaque consommateur que le consommateur et le serveur connaissent tous deux pour hmac, un message, HMAC256, doit être utilisé. Dans la plupart des cas, le mot de passe haché du consommateur est utilisé comme clé secrète.

Le message est normalement construit à partir des données de la requête HTTP, ou même des données personnalisées ajoutées à l'en-tête HTTP. Le message peut inclure:

  1. Horodatage: heure à laquelle la demande est envoyée (UTC ou GMT)
  2. Verbe HTTP: GET, POST, PUT, DELETE.
  3. publier des données et une chaîne de requête,
  4. URL

Sous le capot, l'authentification HMAC serait:

Le consommateur envoie une requête HTTP au serveur Web, après avoir construit la signature (sortie de hash Hmac), le modèle de requête HTTP:

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

Exemple de demande GET:

GET /webapi.hmac/api/values

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

Le message à hash pour obtenir la signature:

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

Exemple de demande POST avec chaîne de requête (la signature ci-dessous n'est pas correcte, il s'agit simplement d'un exemple)

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

Le message à hash pour obtenir la signature

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

Veuillez noter que les données de formulaire et la chaîne de requête doivent être en ordre, de sorte que le code sur le serveur obtienne la chaîne de requête et les données de formulaire pour générer le message correct.

Lorsque la requête HTTP arrive sur le serveur, un filtre d'action d'authentification est implémenté pour analyser la requête afin d'obtenir des informations: verbe HTTP, horodatage, uri, données de formulaire et chaîne de requête, puis basé sur ceux-ci pour construire une signature (utiliser hmac hash) avec le secret. clé (mot de passe haché) sur le serveur.

La clé secrète provient de la base de données avec le nom d'utilisateur sur la requête.

Ensuite, le code serveur compare la signature de la requête à la signature générée; si égal, l'authentification est transmise, sinon, elle échoue.

Le code pour construire la signature:

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

Alors, comment empêcher les attaques par rejeu?

Ajouter une contrainte pour l'horodatage, quelque chose comme:

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

(heure du serveur: heure à laquelle la requête parvient au serveur)

Et, cachez la signature de la requête en mémoire (utilisez MemoryCache, vous devriez le garder dans la limite de temps). Si la demande suivante vient avec la même signature que la demande précédente, elle sera rejetée.

Le code de démonstration est mis comme ici: https://github.com/cuongle/Hmac.WebApi

285
cuongle

Je suggérerais de commencer par les solutions les plus simples - une simple authentification HTTP basique + HTTPS suffira peut-être dans votre scénario.

Si ce n'est pas le cas (par exemple, vous ne pouvez pas utiliser https, ou avez besoin d'une gestion de clé plus complexe), vous pouvez jeter un coup d'œil aux solutions basées sur HMAC proposées par d'autres. Amazon S3 est un bon exemple de cette API ( http://s3.amazonaws.com/doc/s3-developer-guide/RESTAuthentication.html )

J'ai écrit un article de blog sur l'authentification basée sur HMAC dans l'API Web ASP.NET. Il traite à la fois du service API Web et du client API Web et le code est disponible sur bitbucket. http://www.piotrwalat.net/hmac-authentication-in-asp-net-web-api/

Voici un article sur l'authentification de base dans l'API Web: http://www.piotrwalat.net/basic-http-authentication-in-asp-net-web-api-using-message-handlers/

N'oubliez pas que si vous allez fournir une API à des tiers, vous serez probablement également responsable de la livraison des bibliothèques clientes. L'authentification de base présente ici un avantage considérable, car elle est prise en charge immédiatement sur la plupart des plateformes de programmation. HMAC, en revanche, n’est pas aussi standardisé et nécessitera une implémentation personnalisée. Celles-ci doivent être relativement simples mais nécessitent tout de même du travail.

PS Il existe également une option d'utilisation des certificats HTTPS +. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-windows-store-apps/

30
Piotr Walat

Avez-vous essayé DevDefined.OAuth?

Je l'ai utilisé pour sécuriser mon WebApi avec OAuth à deux pattes. Je l'ai également testé avec succès avec PHP clients.

Il est assez facile d'ajouter un support pour OAuth en utilisant cette bibliothèque. Voici comment vous pouvez implémenter le fournisseur pour API Web ASP.NET MVC:

1) Obtenez le code source de DevDefined.OAuth: https://github.com/bittercoder/DevDefined.OAuth - la version la plus récente permet l’extension OAuthContextBuilder.

2) Construisez la bibliothèque et référencez-la dans votre projet API Web.

3) Créez un générateur de contexte personnalisé pour prendre en charge la création d'un contexte à partir de 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) Utilisez ce didacticiel pour créer un fournisseur OAuth: http://code.google.com/p/devdefined-tools/wiki/OAuthProvider . Dans la dernière étape (Exemple d'accès à une ressource protégée), vous pouvez utiliser ce code dans votre attribut 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()
            };
    }
}

J'ai implémenté mon propre fournisseur et je n'ai donc pas testé le code ci-dessus (sauf bien sûr le WebApiOAuthContextBuilder que j'utilise dans mon fournisseur), mais cela devrait fonctionner correctement.

23
Maksymilian Majer

L'API Web a introduit un attribut [Authorize] pour assurer la sécurité. Ceci peut être défini globalement (global.asx)

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

Ou par contrôleur:

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

Bien sûr, votre type d'authentification peut varier et vous pouvez effectuer votre propre authentification. Lorsque cela se produit, vous pouvez trouver utile d'hériter de l'attribut Authorizate et de l'étendre en fonction de vos besoins:

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

Et dans votre contrôleur:

[DemoAuthorize]
public class ValuesController : ApiController{

Voici un lien vers d'autres implémentations personnalisées pour les autorisations WebApi:

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

21
Dalorzo

Si vous souhaitez sécuriser votre API en mode serveur à serveur (pas de redirection vers le site Web pour une authentification à deux branches). Vous pouvez consulter le protocole OAuth2 Client Credentials Grant.

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

J'ai développé une bibliothèque qui peut facilement vous aider à ajouter ce type de support à votre WebAPI. Vous pouvez l'installer en tant que paquet NuGet:

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

La bibliothèque cible .NET Framework 4.5.

Une fois le package ajouté à votre projet, un fichier Lisez-moi sera créé à la racine de votre projet. Vous pouvez consulter ce fichier lisez-moi pour savoir comment configurer/utiliser ce paquet.

À votre santé!

6
Varun Chatterji

dans la suite de la réponse de @ Cuong Le, mon approche pour empêcher une attaque par rejeu serait

// Crypter l'heure Unix côté client à l'aide de la clé privée partagée (ou du mot de passe de l'utilisateur)

// l'envoyer dans le cadre de l'en-tête de la demande au serveur (API Web)

// Décrypter l'heure Unix sur serveur (API Web) à l'aide de la clé privée partagée (ou du mot de passe de l'utilisateur)

// Vérifier la différence de temps entre l'heure Unix du client et celle du serveur, ne doit pas dépasser x s

// si l'ID utilisateur/mot de passe de hachage est correct et que le temps UnixTime déchiffré est à x sec de l'heure du serveur, il s'agit d'une demande valide

3
refactor