Comment sécuriser une API Web ASP.NET [fermé]

397

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 lu beaucoup de choses sur OAuth et cela semble être la norme, mais trouver un bon échantillon avec de la documentation expliquant comment cela fonctionne (et qui fonctionne réellement!) Semble être incroyablement difficile (en particulier pour un débutant à OAuth).

Existe-t-il un exemple qui construit et fonctionne réellement et montre comment l'implémenter?

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

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

J'ai également consulté des blogs suggérant un schéma simple basé sur des jetons (comme celui-ci ) - cela semble réinventer 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.

Que fait tout le monde dans cet espace?

Craig Shearer
la source

Réponses:

292

Mise à jour:

J'ai ajouté ce lien à mon autre réponse comment utiliser l'authentification JWT pour l'API Web ASP.NET ici pour toute personne intéressée par JWT.


Nous avons réussi à appliquer l'authentification HMAC à l'API Web sécurisée, et cela a bien fonctionné. L'authentification HMAC utilise une clé secrète pour chaque consommateur que le consommateur et le serveur connaissent tous les deux pour hmac hacher un message, HMAC256 doit être utilisé. 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 qui sont 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 du hachage 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 à 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 chaîne de requête (la signature ci-dessous n'est pas correcte, 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 de formulaire et la chaîne de requête doivent être en ordre, de sorte que le code sur le serveur récupère la chaîne de requête et les données de formulaire pour générer le message correct.

Lorsque la demande HTTP arrive sur le serveur, un filtre d'action d'authentification est implémenté pour analyser la demande 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 créer une signature (utiliser le hachage hmac) avec le secret clé (mot de passe haché) sur le serveur.

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

Ensuite, le code serveur compare la signature de la demande avec la signature créée; si égal, l'authentification est passée, sinon, elle a échoué.

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 l'attaque de rejeu?

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

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

(heure du serveur: heure de la demande qui arrive au serveur)

Et, mettez en cache la signature de la requête en mémoire (utilisez MemoryCache, devrait rester dans la limite de temps). Si la prochaine demande est accompagnée de la même signature que la précédente, elle sera rejetée.

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

cuongle
la source
2
@James: seul l'horodatage ne semble pas suffisant, pendant peu de temps, ils peuvent simuler la demande et l'envoyer au serveur, je viens de modifier mon message, utiliser les deux serait le meilleur.
cuongle
1
Êtes-vous sûr que cela fonctionne comme il se doit? vous hachez l'horodatage avec le message et mettez ce message en cache. Cela signifierait une signature différente à chaque demande, ce qui rendrait votre signature mise en cache inutile.
Filip Stas du
1
@FilipStas: semble que je ne comprends pas votre argument, la raison d'utiliser Cache ici est d'empêcher les attaques par relais, rien de plus
cuongle
1
@ChrisO: Vous pouvez consulter [cette page] ( jokecamp.wordpress.com/2012/10/21/… ). Je
mettrai à
1
La solution suggérée fonctionne, mais vous ne pouvez pas empêcher l'attaque Man-in-the-Middle, pour cela vous devez implémenter HTTPS
refactor
34

Je suggérerais de commencer par les solutions les plus simples en premier - peut-être qu'une simple authentification de base HTTP + HTTPS est suffisante 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és plus complexe), vous pouvez jeter un œil aux 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 l'API Web ASP.NET. Il traite à la fois du service d'API Web et du client d'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 également très probablement responsable de la livraison des bibliothèques clientes. L'authentification de base a un avantage significatif ici car elle est prise en charge sur la plupart des plates-formes de programmation prêtes à l'emploi. HMAC, d'autre part, n'est pas normalisé et nécessitera une implémentation personnalisée. Celles-ci devraient être relativement simples mais nécessitent encore du travail.

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

Piotr Walat
la source
23

Avez-vous essayé DevDefined.OAuth?

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

Il est assez facile d'ajouter la prise en charge d'OAuth à l'aide de 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 l' OAuthContextBuilderextensibilité.

2) Créez la bibliothèque et référencez-la dans votre projet d'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 aux ressources protégées), vous pouvez utiliser ce code dans votre AuthorizationFilterAttributeattribut:

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, donc je n'ai pas testé le code ci-dessus (sauf bien sûr celui WebApiOAuthContextBuilderque j'utilise dans mon fournisseur) mais cela devrait fonctionner correctement.

Maksymilian Majer
la source
Merci - je vais y jeter un œil, bien que pour l'instant j'ai roulé ma propre solution basée sur HMAC.
Craig Shearer
1
@CraigShearer - salut, vous dites que vous avez roulé la vôtre .. juste eu quelques questions si cela ne vous dérange pas de partager. Je suis dans une position similaire, où j'ai une API Web MVC relativement petite. Les contrôleurs d'API se trouvent aux côtés d'autres contrôleurs / actions qui sont sous l'authentification par formulaire. La mise en œuvre d'OAuth semble exagérée lorsque j'ai déjà un fournisseur d'adhésion que je pourrais utiliser et que je n'ai besoin que de sécuriser une poignée d'opérations. Je veux vraiment une action d'authentification qui renvoie un jeton chiffré - puis utilisé le jeton dans les appels suivants? toute information bienvenue avant de m'engager à implémenter une solution d'authentification existante. Merci!
sambomartin
@Maksymilian Majer - Avez-vous une chance de partager plus en détail comment vous avez implémenté le fournisseur? J'ai des problèmes pour renvoyer des réponses au client.
jlrolin
21

L'API Web a introduit un attribut [Authorize]pour assurer la sécurité. Cela 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 souhaiterez peut-être effectuer votre propre authentification, lorsque cela se produit, vous trouverez peut-être utile d'hériter de l'attribut d'autorisation 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 sur 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/

Dalorzo
la source
Merci pour l'exemple @Dalorzo, mais j'ai quelques problèmes. J'ai regardé le lien ci-joint, mais suivre ces instructions ne fonctionne pas vraiment. J'ai également trouvé les informations nécessaires manquantes. Premièrement, lorsque je crée le nouveau projet, est-il juste de choisir des comptes d'utilisateurs individuels pour l'authentification? Ou dois-je le laisser sans authentification. Je ne reçois pas non plus l'erreur 302 mentionnée, mais j'obtiens une erreur 401. Enfin, comment transmettre les informations nécessaires de ma vue au contrôleur? À quoi doit ressembler mon appel ajax? Btw, j'utilise l'authentification par formulaire pour mes vues MVC. Est-ce un problème?
Amanda
Cela fonctionne à merveille. Tout simplement agréable d'apprendre et de commencer à travailler sur nos propres jetons d'accès.
CodeName47
Un petit commentaire - faites attention AuthorizeAttribute, car il existe deux classes différentes portant le même nom, dans des espaces de noms différents: 1. System.Web.Mvc.AuthorizeAttribute -> pour les contrôleurs MVC 2. System.Web.Http.AuthorizeAttribute -> pour WebApi.
Vitaliy Markitanov
5

Si vous souhaitez sécuriser votre API de serveur à serveur (pas de redirection vers le site Web pour une authentification à 2 pattes). Vous pouvez consulter le protocole d'octroi des informations d'identification client OAuth2.

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

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

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

La bibliothèque cible .NET Framework 4.5.

Une fois que vous avez ajouté le package à votre projet, il crée un fichier Lisez-moi à la racine de votre projet. Vous pouvez consulter ce fichier Lisezmoi pour voir comment configurer / utiliser ce package.

À votre santé!

Varun Chatterji
la source
5
Partagez-vous / fournissez-vous le code source de ce framework en open source?
barrypicker
JFR: First Link is Broken et le package NuGet n'a jamais été mis à jour
Abdul Qayyum
3

dans la continuité de la réponse de @ Cuong Le, mon approche pour empêcher l'attaque de rejeu serait

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

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

// Déchiffrer l'heure Unix sur le 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 l'heure Unix du serveur, ne doit pas être supérieure à x sec

// si l'ID utilisateur / le mot de passe de hachage sont corrects et que l'UnixTime déchiffré est à moins de x secondes de l'heure du serveur, alors c'est une demande valide

refactor
la source