Pourquoi AuthorizeAttribute redirige-t-il vers la page de connexion pour les échecs d'authentification et d'autorisation?

265

Dans ASP.NET MVC, vous pouvez baliser une méthode de contrôleur avec AuthorizeAttribute, comme ceci:

[Authorize(Roles = "CanDeleteTags")]
public void Delete(string tagName)
{
    // ...
}

Cela signifie que si l'utilisateur actuellement connecté n'est pas dans le rôle "CanDeleteTags", la méthode du contrôleur ne sera jamais appelée.

Malheureusement, en cas d'échec, AuthorizeAttributerenvoie HttpUnauthorizedResult, qui renvoie toujours le code d'état HTTP 401. Cela provoque une redirection vers la page de connexion.

Si l'utilisateur n'est pas connecté, cela est parfaitement logique. Cependant, si l'utilisateur est déjà connecté, mais n'a pas le rôle requis, il est difficile de le renvoyer à la page de connexion.

Il semble que cela AuthorizeAttributeconfond l'authentification et l'autorisation.

Cela semble être un peu une erreur dans ASP.NET MVC, ou est-ce que je manque quelque chose?

J'ai dû préparer un DemandRoleAttributequi sépare les deux. Lorsque l'utilisateur n'est pas authentifié, il renvoie HTTP 401, en l'envoyant à la page de connexion. Lorsque l'utilisateur est connecté, mais n'est pas dans le rôle requis, il crée un à la NotAuthorizedResultplace. Actuellement, cela redirige vers une page d'erreur.

Je n'avais sûrement pas à faire ça?

Roger Lipscombe
la source
10
Excellente question et je suis d'accord, il devrait lancer un statut HTTP non autorisé.
Pure.Krome
3
J'aime ta solution, Roger. Même si ce n'est pas le cas.
Jon Davis
Ma page de connexion est cochée pour simplement rediriger l'utilisateur vers ReturnUrl, s'il est déjà authentifié. J'ai donc réussi à créer une boucle infinie de 302 redirections: D woot.
juhan_h
1
Regardez ça .
Jogi
Roger, bon article sur votre solution - red-gate.com/simple-talk/dotnet/asp-net/… Il semble que votre solution soit le seul moyen de le faire proprement
Craig

Réponses:

305

Quand il a été développé pour la première fois, System.Web.Mvc.AuthorizeAttribute faisait la bonne chose - les révisions plus anciennes de la spécification HTTP utilisaient le code d'état 401 pour "non autorisé" et "non authentifié".

De la spécification d'origine:

Si la demande contient déjà des informations d'identification d'autorisation, la réponse 401 indique que l'autorisation a été refusée pour ces informations d'identification.

En fait, vous pouvez voir la confusion ici - il utilise le mot «autorisation» quand il signifie «authentification». Dans la pratique quotidienne, cependant, il est plus logique de renvoyer un 403 Interdit lorsque l'utilisateur est authentifié mais non autorisé. Il est peu probable que l'utilisateur ait un deuxième ensemble d'informations d'identification qui lui donnerait accès - une mauvaise expérience utilisateur tout autour.

Tenez compte de la plupart des systèmes d'exploitation - lorsque vous essayez de lire un fichier auquel vous n'êtes pas autorisé à accéder, aucun écran de connexion ne s'affiche.

Heureusement, les spécifications HTTP ont été mises à jour (juin 2014) pour lever l'ambiguïté.

Extrait de "Hyper Text Transport Protocol (HTTP / 1.1): Authentication" (RFC 7235):

Le code d'état 401 (non autorisé) indique que la demande n'a pas été appliquée car il manque des informations d'identification d'authentification valides pour la ressource cible.

Extrait de "Hypertext Transfer Protocol (HTTP / 1.1): Semantics and Content" (RFC 7231):

Le code d'état 403 (interdit) indique que le serveur a compris la demande mais refuse de l'autoriser.

Chose intéressante, au moment où ASP.NET MVC 1 a été publié, le comportement d'AutorizeAttribute était correct. Maintenant, le comportement est incorrect - la spécification HTTP / 1.1 a été corrigée.

Plutôt que d'essayer de modifier les redirections de la page de connexion d'ASP.NET, il est plus facile de résoudre le problème à la source. Vous pouvez créer un nouvel attribut avec le même nom ( AuthorizeAttribute) dans l'espace de noms par défaut de votre site Web (c'est très important), puis le compilateur le récupérera automatiquement au lieu de celui standard de MVC. Bien sûr, vous pouvez toujours donner un nouveau nom à l'attribut si vous préférez adopter cette approche.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
public class AuthorizeAttribute : System.Web.Mvc.AuthorizeAttribute
{
    protected override void HandleUnauthorizedRequest(System.Web.Mvc.AuthorizationContext filterContext)
    {
        if (filterContext.HttpContext.Request.IsAuthenticated)
        {
            filterContext.Result = new System.Web.Mvc.HttpStatusCodeResult((int)System.Net.HttpStatusCode.Forbidden);
        }
        else
        {
            base.HandleUnauthorizedRequest(filterContext);
        }
    }
}
Chasseur d'ombre
la source
52
+1 Très bonne approche. Une petite suggestion: au lieu de vérifier filterContext.HttpContext.User.Identity.IsAuthenticated, vous pouvez simplement vérifier filterContext.HttpContext.Request.IsAuthenticated, ce qui est livré avec des contrôles nuls intégrés. Voir stackoverflow.com/questions/1379566/…
Daniel Liuzzi
> Vous pouvez créer un nouvel attribut avec le même nom (AuthorizeAttribute) dans l'espace de noms par défaut de votre site Web, puis le compilateur le récupérera automatiquement au lieu de celui standard de MVC. Il en résulte une erreur: le type ou l'espace de noms «Autoriser» est introuvable (manque-t-il une directive ou une référence d'assembly?) Les deux utilisant System.Web.Mvc; et l'espace de noms de ma classe AuthorizeAttribute personnalisée sont référencés dans le contrôleur. Pour résoudre ce problème, j'ai dû utiliser [MyNamepace.Authorize]
stormwild
2
@DePeter la spécification ne dit jamais rien sur une redirection, alors pourquoi une redirection est-elle une meilleure solution? Cela seul tue les demandes ajax sans un hack en place pour le résoudre.
Adam Tuliper - MSFT
1
Cela devrait être enregistré sur MS Connect car il s'agit clairement d'un bogue comportemental. Merci.
Tony Wall
BTW, pourquoi sommes-nous redirigés vers la page de connexion? Pourquoi ne pas simplement sortir un code 401 et la page de connexion directement dans la même demande?
SandRock
25

Ajoutez ceci à votre fonction Login

// User was redirected here because of authorization section
if (User.Identity != null && User.Identity.IsAuthenticated)
    Response.Redirect("Unauthorized.aspx");

Lorsque l'utilisateur y est redirigé mais est déjà connecté, il affiche la page non autorisée. S'ils ne sont pas connectés, il tombe et affiche la page de connexion.

Alan Jackson
la source
18
▶Load est un mojo Webforms
Chance
2
@Chance - faites cela dans la ActionMethod par défaut pour le contrôleur qui est appelé là où FormsAuthencation a été configuré pour appeler.
Pure.Krome
Cela fonctionne vraiment bien, mais pour MVC, cela devrait être quelque chose comme if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");Non autorisé est un nom de route défini.
Moses Machua
Vous demandez donc une ressource, vous êtes redirigé vers une page de connexion et vous êtes redirigé à nouveau vers une page 403? Cela me semble mauvais. Je ne peux même pas tolérer du tout une redirection. OMI cette chose est de toute façon très mal construite.
SandRock
3
Selon votre solution, si vous êtes déjà connecté et accédez à la page de connexion en tapant l'URL ... cela vous renverrait à une page non autorisée. ce qui n'est pas juste.
Rajshekar Reddy
4

J'ai toujours pensé que cela avait du sens. Si vous êtes connecté et que vous essayez d'accéder à une page qui nécessite un rôle que vous n'avez pas, vous êtes redirigé vers l'écran de connexion vous demandant de vous connecter avec un utilisateur qui a le rôle.

Vous pouvez ajouter une logique à la page de connexion qui vérifie si l'utilisateur est déjà authentifié. Vous pouvez ajouter un message amical qui explique pourquoi ils ont de nouveau été bousculés là-bas.

Rob
la source
4
J'ai le sentiment que la plupart des gens n'ont pas tendance à avoir plus d'une identité pour une application Web donnée. S'ils le font, alors ils sont assez intelligents pour penser "mon ID actuel n'a pas de mojo, je vais me reconnecter comme l'autre".
Roger Lipscombe
Bien que votre autre point sur l'affichage de quelque chose sur la page de connexion soit bon. Merci.
Roger Lipscombe
4

Malheureusement, vous avez affaire au comportement par défaut de l'authentification par formulaires ASP.NET. Il existe une solution de contournement (je ne l'ai pas essayé) discutée ici:

http://www.codeproject.com/KB/aspnet/Custon401Page.aspx

(Ce n'est pas spécifique à MVC)

Je pense que dans la plupart des cas, la meilleure solution est de restreindre l'accès aux ressources non autorisées avant que l'utilisateur ne tente d'y arriver. En supprimant / grisant le lien ou le bouton qui pourrait les mener à cette page non autorisée.

Il serait probablement intéressant d'avoir un paramètre supplémentaire sur l'attribut pour spécifier où rediriger un utilisateur non autorisé. Mais en attendant, je considère l'AutorizeAttribute comme un filet de sécurité.

Keltex
la source
Je prévois également de supprimer le lien basé sur l'autorisation (j'ai vu une question ici à ce sujet quelque part), donc je coderai une méthode d'extension HtmlHelper plus tard.
Roger Lipscombe
1
Je dois quand même empêcher l'utilisateur d'accéder directement à l'URL, ce qui est le sens de cet attribut. Je ne suis pas trop satisfait de la solution Custom 401 (semble un peu globale), donc je vais essayer de modéliser mon NotAuthorizedResult sur RedirectToRouteResult ...
Roger Lipscombe
0

Essayez ceci dans votre dans le gestionnaire Application_EndRequest de votre fichier Global.ascx

if (HttpContext.Current.Response.Status.StartsWith("302") && HttpContext.Current.Request.Url.ToString().Contains("/<restricted_path>/"))
{
    HttpContext.Current.Response.ClearContent();
    Response.Redirect("~/AccessDenied.aspx");
}
Kareem Cambridge
la source
0

Si vous utilisez aspnetcore 2.0, utilisez ceci:

using System;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;

namespace Core
{
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class AuthorizeApiAttribute : Microsoft.AspNetCore.Authorization.AuthorizeAttribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            var user = context.HttpContext.User;

            if (!user.Identity.IsAuthenticated)
            {
                context.Result = new UnauthorizedResult();
                return;
            }
        }
    }
}
Greg Gum
la source
0

Dans mon cas, le problème était "La spécification HTTP a utilisé le code d'état 401 pour" non autorisé "et" non authentifié "". Comme l'a dit ShadowChaser.

Cette solution fonctionne pour moi:

if (User != null &&  User.Identity.IsAuthenticated && Response.StatusCode == 401)
{
    //Do whatever

    //In my case redirect to error page
    Response.RedirectToRoute("Default", new { controller = "Home", action = "ErrorUnauthorized" });
}
César León
la source