web-dev-qa-db-fra.com

Comment personnaliser l'API Web ASP.NET AuthorizeAttribute pour des exigences inhabituelles

J'hérite de System.Web.Http.AuthorizeAttribute pour créer une routine d'autorisation/d'authentification personnalisée pour répondre à certaines exigences inhabituelles pour une application Web développée à l'aide d'ASP. NET MVC 4. Cela ajoute de la sécurité à l'API Web utilisée pour les appels Ajax à partir du client Web. Les exigences sont les suivantes:

  1. L'utilisateur doit se connecter à chaque fois qu'il effectue une transaction pour vérifier que quelqu'un d'autre ne s'est pas rendu sur le poste de travail après qu'une personne s'est connectée et est partie.
  2. Les rôles ne peuvent pas être attribués aux méthodes de service Web au moment du programme. Ils doivent être attribués au moment de l'exécution afin qu'un administrateur puisse le configurer. Ces informations sont stockées dans la base de données système.

Le client Web est un application à page unique (SPA) donc l'authentification par formulaire typique ne fonctionne pas aussi bien, mais j'essaie de réutiliser autant de l'infrastructure de sécurité ASP.NET que possible pour répondre aux exigences . Le AuthorizeAttribute personnalisé fonctionne très bien pour l'exigence 2 sur la détermination des rôles associés à une méthode de service Web. J'accepte trois paramètres, le nom de l'application, le nom de la ressource et l'opération pour déterminer quels rôles sont associés à une méthode.

public class DoThisController : ApiController
{
    [Authorize(Application = "MyApp", Resource = "DoThis", Operation = "read")]
    public string GetData()
    {
        return "We did this.";
    }
}

Je remplace la méthode OnAuthorization pour obtenir les rôles et authentifier l'utilisateur. Étant donné que l'utilisateur doit être authentifié pour chaque transaction, je réduis les bavardages en effectuant l'authentification et l'autorisation au cours de la même étape. J'obtiens les informations d'identification des utilisateurs du client Web en utilisant une authentification de base qui transmet les informations d'identification cryptées dans l'en-tête HTTP. Donc ma méthode OnAuthorization ressemble à ceci:

public override void OnAuthorization(HttpActionContext actionContext)
{

     string username;
     string password;
     if (GetUserNameAndPassword(actionContext, out username, out password))
     {
         if (Membership.ValidateUser(username, password))
         {
             FormsAuthentication.SetAuthCookie(username, false);
             base.Roles = GetResourceOperationRoles();
         }
         else
         {
             FormsAuthentication.SignOut();
             base.Roles = "";
         }
     }
     else
     {
         FormsAuthentication.SignOut();
         base.Roles = "";
     }
     base.OnAuthorization(actionContext);
 }

GetUserNameAndPassword récupère les informations d'identification de l'en-tête HTTP. J'utilise ensuite le Membership.ValidateUser pour valider les informations d'identification. J'ai un fournisseur d'appartenance personnalisé et un fournisseur de rôles connectés pour accéder à une base de données personnalisée. Si l'utilisateur est authentifié, je récupère les rôles pour la ressource et l'opération. De là, j'utilise la base OnAuthorization pour terminer le processus d'autorisation. Voici où ça tombe en panne.

Si l'utilisateur est authentifié, j'utilise les méthodes d'authentification par formulaire standard pour connecter l'utilisateur (FormsAuthentication.SetAuthCookie) et s'il échoue, je le déconnecte (FormsAuthentication.SignOut). Mais le problème semble être que la classe de base OnAuthorization n'a pas accès à Principal qui est mis à jour pour que IsAuthenticated soit défini sur la valeur correcte. C'est toujours un pas en arrière. Et je suppose qu'il utilise une valeur mise en cache qui n'est pas mise à jour jusqu'à ce qu'il y ait un aller-retour vers le client Web.

Donc, tout cela mène à ma question spécifique qui est, existe-t-il une autre façon de définir IsAuthenticated à la valeur correcte pour le courant Principal sans utiliser de cookies? Il me semble que les cookies ne s'appliquent pas vraiment dans ce scénario spécifique où je dois m'authentifier à chaque fois. La raison pour laquelle je sais IsAuthenticated n'est pas définie sur la valeur correcte est que je remplace également la HandleUnauthorizedRequest méthode à ceci:

 protected override void HandleUnauthorizedRequest(HttpActionContext filterContext)
 {
     if (((System.Web.HttpContext.Current.User).Identity).IsAuthenticated)
     {
         filterContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
     }
     else
     {
         base.HandleUnauthorizedRequest(filterContext);
     }
 }

Cela me permet de renvoyer un code d'état Interdit au client Web si l'échec est dû à une autorisation plutôt qu'à une authentification et il peut répondre en conséquence.

Alors, quelle est la bonne façon de définir IsAuthenticated pour le principe actuel dans ce scénario?

35
Kevin Junghans

La meilleure solution pour mon scénario semble être de contourner complètement la base OnAuthorization. Étant donné que je dois m'authentifier à chaque fois, les cookies et la mise en cache du principe ne sont pas très utiles. Voici donc la solution que j'ai trouvée:

public override void OnAuthorization(HttpActionContext actionContext)
{
    string username;
    string password;

    if (GetUserNameAndPassword(actionContext, out username, out password))
    {
        if (Membership.ValidateUser(username, password))
        {
            if (!isUserAuthorized(username))
                actionContext.Response = 
                    new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            actionContext.Response = 
                new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);
        }
    }
    else
    {
        actionContext.Response = 
            new HttpResponseMessage(System.Net.HttpStatusCode.BadRequest);
    }
}

J'ai développé ma propre méthode pour valider les rôles appelés isUserAuthorized et je n'utilise plus la base OnAuthorization car il vérifie le courant Principle pour voir si elle isAuthenticated. IsAuthenticated autorise uniquement les get, donc je ne sais pas comment le définir autrement, et je ne semble pas avoir besoin du courant Principe. Testé cela et cela fonctionne bien.

Toujours intéressé si quelqu'un a une meilleure solution ou peut voir des problèmes avec celui-ci.

38
Kevin Junghans

Pour ajouter à la réponse déjà acceptée: Vérification du code source actuel (aspnetwebstack.codeplex.com) pour System.Web.Http.AuthorizeAttribute, Il semble que la documentation est obsolète. La base OnAuthorization() appelle/vérifie uniquement les statiques privées SkipAuthorization() (qui vérifie simplement si AllowAnonymousAttribute est utilisé en contexte pour contourner le reste de la vérification d'authentification). Ensuite, s'il n'est pas ignoré, OnAuthorization() appelle le public IsAuthorized() et si cet appel échoue, il appelle alors le virtuel protégé HandleUnauthorizedRequest(). Et c'est tout ce qu'il fait ...

public override void OnAuthorization(HttpActionContext actionContext)
{
    if (actionContext == null)
    {
        throw Error.ArgumentNull("actionContext");
    }

    if (SkipAuthorization(actionContext))
    {
        return;
    }

    if (!IsAuthorized(actionContext))
    {
        HandleUnauthorizedRequest(actionContext);
    }
}

En regardant à l'intérieur de IsAuthorized(), c'est là que Principle est comparé aux rôles et aux utilisateurs. Donc, remplacer IsAuthorized() par ce que vous avez ci-dessus au lieu de OnAuthorization() serait la voie à suivre. Là encore, vous devrez probablement remplacer soit OnAuthorization() ou HandleUnauthorizedRequest() de toute façon pour décider quand renvoyer une réponse 401 par rapport à une réponse 403.

25
bob-the-destroyer

Pour ajouter à la réponse absolument correcte de Kevin, je voudrais dire que je peux la modifier légèrement pour tirer parti du chemin d'accès du framework .NET existant pour l'objet de réponse afin de garantir que le code en aval dans le framework (ou d'autres consommateurs) ne soit pas affecté négativement. par une idiosyncrasie étrange qui ne peut être prédite.

Concrètement, cela signifie utiliser ce code:

actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Unauthorized, REQUEST_NOT_AUTHORIZED);

plutôt que:

actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Unauthorized);

REQUEST_NOT_AUTHORIZED est:

private const string REQUEST_NOT_AUTHORIZED = "Authorization has been denied for this request.";

J'ai tiré ce string du SRResources.RequestNotAuthorized définition dans le framework .NET.

Excellente réponse Kevin! J'ai implémenté le mien de la même manière parce que l'exécution de OnAuthorization dans la classe de base n'avait aucun sens parce que je vérifiais un en-tête HTTP personnalisé pour notre application et que je ne voulais pas du tout vérifier le principal car il n'y avait pas 'Ton.

4
Mike Perrenoud