403 votes

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 des développeurs tiers utiliseront pour accéder aux données de mon application.

J'ai lu beaucoup de choses sur OAuth et il semble que ce soit la norme, mais trouver un bon exemple avec une documentation expliquant comment il fonctionne (et qui fonctionne vraiment !) semble être incroyablement difficile (surtout pour un novice en matière d'OAuth).

Existe-t-il un exemple qui fonctionne et qui montre comment mettre en œuvre ce système ?

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

  • DotNetOAuth - la documentation est désespérante du point de vue d'un débutant
  • Thinktecture - Je n'arrive pas à le construire.

J'ai également consulté des blogs suggérant un système simple basé sur des jetons (tel que ce ) - cela ressemble à réinventer la roue mais cela a l'avantage d'être conceptuellement assez simple.

Il semble qu'il y ait beaucoup de questions de ce type sur l'OS mais pas de bonnes réponses.

Que fait-on dans cet espace ?

299voto

Cuong Le Points 29324

Nous avons réussi à appliquer l'authentification HMAC pour sécuriser l'Api Web et cela a bien fonctionné. Fondamentalement, l'authentification HMAC utilise une clé secrète pour chaque consommateur que le consommateur et le serveur connaissent tous deux pour hacher 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 de données personnalisées qui sont ajoutées dans l'en-tête HTTP, le message peut inclure :

  1. Timestamp : heure à laquelle la demande est envoyée (heure UTC ou GMT)
  2. Verbe HTTP : GET, POST, PUT, DELETE.
  3. les données du message et la 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 du hachage hmac), le modèle de la 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 à hacher 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 querystring (la signature ci-dessous n'est pas correcte, c'est juste 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 à hacher 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 du formulaire et la chaîne de requête doivent être dans l'ordre, afin que le code sur le serveur obtienne la chaîne de requête et les données du formulaire pour construire le message correct.

Lorsqu'une demande HTTP arrive au serveur, un filtre d'action d'authentification est mis en œuvre pour analyser la demande et obtenir des informations : Le verbe HTTP, l'horodatage, l'uri, les données du formulaire et la chaîne de requête, puis, sur la base de ces informations, il construit une signature (en utilisant un hachage hmac) avec une clé secrète (un mot de passe haché) sur le serveur.

La clé secrète est obtenue à partir de la base de données avec le nom d'utilisateur sur la demande.

Ensuite, le code du serveur compare la signature de la demande avec la signature construite, si elle est égale, l'authentification est réussie, 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 prévenir les attaques par relais ?

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

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

(servertime : heure de réception de la demande par le serveur)

Et, mettre en cache la signature de la demande en mémoire (utiliser MemoryCache, devrait garder en limite de temps). Si la demande suivante a la même signature que la précédente, elle sera rejetée.

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

34voto

Piotr Walat Points 648

Je suggère de commencer par les solutions les plus simples - peut-être qu'une simple authentification HTTP de base + HTTPS est suffisante dans votre scénario ?

Si ce n'est pas le cas (par exemple, vous ne pouvez pas utiliser https, ou vous avez besoin d'une gestion des clés plus complexe), vous pouvez envisager des solutions basées sur HMAC, comme suggéré par d'autres. Un bon exemple d'une telle api serait Amazon S3 ( 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 ASP.NET Web API. Il traite à la fois du service Web API et du client Web API 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 fournissez une API à des tiers, vous serez très probablement responsable de la fourniture de bibliothèques clientes. L'authentification de base a un grand avantage ici car elle est prise en charge par la plupart des plates-formes de programmation dès le départ. HMAC, par contre, n'est pas aussi standardisé et nécessitera une implémentation personnalisée. Celles-ci devraient être relativement simples, mais nécessitent tout de même du travail.

PS. Il existe également une option permettant d'utiliser le protocole HTTPS et les certificats. http://www.piotrwalat.net/client-certificate-authentication-in-asp-net-web-api-and-Windows-store-apps/

23voto

Maksymilian Majer Points 1548

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 des clients PHP.

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

1) Obtenez le code source de DevDefined.OAuth : https://github.com/bittercoder/DevDefined.OAuth - la dernière version permet OAuthContextBuilder l'extensibilité.

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

3) Créer un constructeur de contexte personnalisé pour prendre en charge la construction d'un contexte à partir des éléments suivants 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 tutoriel 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 AuthorizationFilterAttribute attribut :

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 mis en place mon propre fournisseur d'accès et je n'ai donc pas testé le code ci-dessus (à l'exception, bien sûr, de l'option WebApiOAuthContextBuilder que j'utilise dans mon fournisseur) mais cela devrait fonctionner correctement.

22voto

Dalorzo Points 6449

L'API Web a introduit un attribut [Authorize] pour assurer la sécurité. Ce paramètre peut être défini de manière globale (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 vouloir effectuer votre propre authentification. Dans ce cas, il peut être utile d'hériter de l'attribut Authorizate et de l'étendre pour répondre à 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/

5voto

Sameer Vartak Points 131

J'ai aimé Cuong Le's et nous allons nous pencher sur la question. Une partie de notre API ne nécessite pas d'authentification, pour le reste nous utilisons Authentification de base sur HTTPs et il semble qu'il nous serve bien, au moins pour la version 1. Il n'est probablement pas aussi sécurisé qu'OAuth, mais il n'est pas non plus aussi compliqué à mettre en œuvre.

Nous avons créé BasicAuthenticationAttribute qui implémente l'interface IAuthorizationFilter. Chaque contrôleur d'API ou point final individuel nécessitant une authentification doit porter cet attribut, et le point final ne sera exécuté que si l'authentification est réussie.

C'est suffisamment sûr pour nous car notre API est utilisée par des applications natives et nous obligeons les clients à ne transmettre que le hachage MD5 du mot de passe dans l'en-tête d'autorisation, de sorte que le véritable mot de passe n'est pas exposé. Nous imposons une sécurité supplémentaire aux clients en les obligeant à utiliser des clés d'API publiques/privées. Voici comment cela fonctionne.

  1. Les clients doivent s'assurer qu'ils fournissent l'en-tête d'autorisation conformément aux spécifications de l'Auth de base (en fait, une chaîne de caractères concaténée nom d'utilisateur + ';' mot de passe (hachage MD5) convertie en Base64).
  2. Sur le serveur, BasicAuthorizationFilter vérifie l'existence d'un en-tête d'autorisation et, s'il n'est pas fourni, le demande.
  3. Analyse le nom d'utilisateur et le mot de passe de l'en-tête d'autorisation, puis vérifie les informations d'identification dans le CACHE (des demandes précédentes), si elles sont trouvées, compare le hachage du mot de passe et si elles ne correspondent pas, renvoie le code d'erreur UnAuthorized (401).
  4. Si les informations d'identification ne sont pas dans le CACHE, il faut les trouver dans la base de données, si le mot de passe ne correspond pas, il faut renvoyer 401.
  5. Ajoutez les informations d'identification au CACHE, pour que la prochaine authentification soit rapide.
  6. Appelez le point de terminaison du contrôleur API.

Voici le code...

    public async Task<HttpResponseMessage> ExecuteAuthorizationFilterAsync(System.Web.Http.Controllers.HttpActionContext actionContext,
                                                                          System.Threading.CancellationToken cancellationToken,
                                                                          Func<System.Threading.Tasks.Task<HttpResponseMessage>> continuation)
    {
        // Check to see if we have the Authorization header, if not then as per Basic Authentication specification
        // We have to ask for it.
        if (actionContext.Request.Headers.Authorization == null)
        {
            var response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
            response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue("Basic", "realm=\"" + _WEBAPI_REALM + "\""));

            actionContext.Response = response;
            return response;
        }

        // OK, so we do have Authorization header, lets parse the user name.
        try
        {
            string authToken = actionContext.Request.Headers.Authorization.Parameter;
            string decodedToken = System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(authToken));

            string username = decodedToken.Substring(0, decodedToken.IndexOf(_AUTH_TOKEN_SEPERATOR));
            string password = decodedToken.Substring(decodedToken.IndexOf(_AUTH_TOKEN_SEPERATOR) + 1);

            // If you are using CACHING then before we go and check the Database lets see if we have the identity in the local cache.
            // This could be asp.net cache, Azure or anything...
            string hashedToken = WorldRemit.Core.Utilities.HashUtilities.CreateHash(decodedToken);
            string cachedKey = string.Format("Account/{0}", hashedToken);

            bool authorized = false;
            var cachedIdentity = await CachedRepository.Get<ApiIdentity>(cachedKey, false);
            {
                if (cachedIdentity != null &&
                    cachedIdentity.IsAuthenticated &&
                    cachedIdentity.LastActivityTimestamp.HasValue &&
                    DateTime.Now.Subtract(cachedIdentity.LastActivityTimestamp.Value).TotalMinutes < _CacheExpirythresholdInMinutes &&
                    !string.IsNullOrWhiteSpace(cachedIdentity.Token))
                {
                    // we found cached identity, just compare the AUTH token, if not the same then we know these are wrong
                    // username/passord.
                    if (!hashedToken.Equals(cachedIdentity.Token, StringComparison.Ordinal))
                    {
                        actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
                        return actionContext.Response;
                    }

                    cachedIdentity.LastActivityTimestamp = DateTime.Now;
                    await CachedRepository.Put(cachedKey, cachedIdentity, false);

                    authorized = true;
                }
            }

            if (!authorized)
            {
                // Now find a user for this username. and then validate the credentials (only compare the hashed password)
                using (var connection = SQLHelper.InitializeConnection())
                {
                    // YOUR CODE TO FIND THE CREDENTIALS IN THE DB AND VALIDATE THEM.

                    // Assuming the crdentials are right, add them to the CACHE, so the subsequent API calls don't have
                    // to make round trip to the Database.

                    ApiIdentity identity = new ApiIdentity(hashedToken);
                    identity.AuthenticationType = "Basic";
                    identity.LastActivityTimestamp = DateTime.Now;
                    identity.IsAuthenticated = true;

                    await CachedRepository.Put(cachedKey, identity, false);
                }
            }
        }
        catch (Exception ex)
        {
            // Let the consumer know that the authorization header was wrong!
            actionContext.Response = new System.Net.Http.HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
            return actionContext.Response;
        }

        // Now run the actual API Controller endpoint.
        return await continuation.Invoke();
    }

J'espère que cela vous aidera !

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X