¿Por qué AuthorizeAttribute redirige a la página de inicio de sesión por fallas de autenticación y autorización?

265

En ASP.NET MVC, puede marcar un método de controlador con AuthorizeAttribute, como este:

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

Esto significa que, si el usuario conectado actualmente no está en el rol "CanDeleteTags", nunca se llamará al método del controlador.

Desafortunadamente, para fallas, AuthorizeAttributeretornos HttpUnauthorizedResult, que siempre devuelve el código de estado HTTP 401. Esto provoca una redirección a la página de inicio de sesión.

Si el usuario no ha iniciado sesión, esto tiene mucho sentido. Sin embargo, si el usuario ya ha iniciado sesión, pero no está en el rol requerido, es confuso enviarlo nuevamente a la página de inicio de sesión.

Parece que AuthorizeAttributecombina la autenticación y la autorización.

Esto parece un poco un descuido en ASP.NET MVC, ¿o me falta algo?

He tenido que cocinar una DemandRoleAttributeque separe a los dos. Cuando el usuario no está autenticado, devuelve HTTP 401 y lo envía a la página de inicio de sesión. Cuando el usuario inicia sesión, pero no está en el rol requerido, crea un NotAuthorizedResultlugar. Actualmente esto redirige a una página de error.

¿Seguramente no tuve que hacer esto?

Roger Lipscombe
fuente
10
Excelente pregunta y estoy de acuerdo, debería arrojar un estado HTTP no autorizado.
Pure.Krome
3
Me gusta tu solución, Roger. Incluso si no lo haces.
Jon Davis
Mi página de inicio de sesión tiene una marca de verificación para simplemente redirigir al usuario a ReturnUrl, si ya está autenticado. Así que logré crear un bucle infinito de 302 redirecciones: D woot.
juhan_h
1
Mira esto .
Jogi
Roger, buen artículo sobre tu solución: red-gate.com/simple-talk/dotnet/asp-net/… Parece que tu solución es la única forma de hacerlo limpiamente
Craig

Respuestas:

305

Cuando se desarrolló por primera vez, System.Web.Mvc.AuthorizeAttribute estaba haciendo lo correcto: las revisiones anteriores de la especificación HTTP usaban el código de estado 401 tanto para "no autorizado" como "no autenticado".

De la especificación original:

Si la solicitud ya incluía credenciales de autorización, la respuesta 401 indica que se ha rechazado la autorización para esas credenciales.

De hecho, puede ver la confusión allí mismo: utiliza la palabra "autorización" cuando significa "autenticación". Sin embargo, en la práctica diaria, tiene más sentido devolver un 403 Prohibido cuando el usuario está autenticado pero no autorizado. Es poco probable que el usuario tenga un segundo conjunto de credenciales que les otorgue acceso, mala experiencia del usuario en general.

Considere la mayoría de los sistemas operativos: cuando intenta leer un archivo al que no tiene permiso de acceso, no se le muestra una pantalla de inicio de sesión.

Afortunadamente, las especificaciones HTTP se actualizaron (junio de 2014) para eliminar la ambigüedad.

Desde "Protocolo de transporte de hipertexto (HTTP / 1.1): Autenticación" (RFC 7235):

El código de estado 401 (no autorizado) indica que la solicitud no se ha aplicado porque carece de credenciales de autenticación válidas para el recurso de destino.

Del "Protocolo de transferencia de hipertexto (HTTP / 1.1): semántica y contenido" (RFC 7231):

El código de estado 403 (Prohibido) indica que el servidor entendió la solicitud pero se niega a autorizarla.

Curiosamente, en el momento en que se lanzó ASP.NET MVC 1, el comportamiento de AuthorizeAttribute era correcto. Ahora, el comportamiento es incorrecto: se corrigió la especificación HTTP / 1.1.

En lugar de intentar cambiar las redirecciones de la página de inicio de sesión de ASP.NET, es más fácil solucionar el problema en la fuente. Puede crear un nuevo atributo con el mismo nombre ( AuthorizeAttribute) en el espacio de nombres predeterminado de su sitio web (esto es muy importante), luego el compilador lo recogerá automáticamente en lugar del estándar de MVC. Por supuesto, siempre podría darle un nuevo nombre al atributo si prefiere adoptar ese enfoque.

[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);
        }
    }
}
Perseguidor de sombras
fuente
52
+1 Muy buen enfoque. Una pequeña sugerencia: en lugar de verificar filterContext.HttpContext.User.Identity.IsAuthenticated, simplemente puede verificar filterContext.HttpContext.Request.IsAuthenticated, que viene con cheques nulos integrados. Consulte stackoverflow.com/questions/1379566/…
Daniel Liuzzi
> Puede crear un nuevo atributo con el mismo nombre (AuthorizeAttribute) en el espacio de nombres predeterminado de su sitio web, luego el compilador lo recogerá automáticamente en lugar del estándar de MVC. Esto da como resultado un error: no se pudo encontrar el tipo o el espacio de nombres 'Autorizar' (¿falta una directiva o una referencia de ensamblado?) Ambos utilizando System.Web.Mvc; y el espacio de nombres para mi clase personalizada AuthorizeAttribute se hace referencia en el controlador. Para resolver esto tuve que usar [MyNamepace.Authorize]
stormwild
2
@DePeter la especificación nunca dice nada acerca de una redirección, entonces, ¿por qué una redirección es una mejor solución? Esto solo mata las solicitudes de ajax sin un truco para resolverlo.
Adam Tuliper - MSFT
1
Eso debe registrarse en MS Connect porque es claramente un error de comportamiento. Gracias.
Tony Wall
Por cierto, ¿por qué nos redirigen a la página de inicio de sesión? ¿Por qué no simplemente generar un código 401 y la página de inicio de sesión directamente dentro de la misma solicitud?
SandRock
25

Agregue esto a su función de carga de página de inicio de sesión:

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

Cuando el usuario es redirigido allí pero ya ha iniciado sesión, muestra la página no autorizada. Si no han iniciado sesión, falla y muestra la página de inicio de sesión.

Alan Jackson
fuente
18
Page_Load es un mojo de formularios web
Chance
2
@Chance: luego haga eso en el ActionMethod predeterminado para el controlador que se llama donde se ha configurado FormsAuthencation para llamar.
Pure.Krome
En realidad, esto funciona muy bien, aunque para MVC debería ser algo así como if (User.Identity != null && User.Identity.IsAuthenticated) return RedirectToRoute("Unauthorized");cuando no autorizada es un nombre de ruta definida.
Moses Machua
¿Entonces solicita un recurso, es redirigido a una página de inicio de sesión y vuelve a ser redirigido a una página 403? Me parece mal Ni siquiera puedo tolerar una redirección en absoluto. OMI, esta cosa está muy mal construida de todos modos.
SandRock
3
Según su solución, si ya ha iniciado sesión y va a la página de inicio de sesión escribiendo la URL ... esto lo llevará a una página no autorizada. lo cual no está bien.
Rajshekar Reddy
4

Siempre pensé que esto tenía sentido. Si ha iniciado sesión e intenta acceder a una página que requiere un rol que no tiene, se le reenvía a la pantalla de inicio de sesión y le solicita que inicie sesión con un usuario que sí lo tiene.

Puede agregar lógica a la página de inicio de sesión que verifica si el usuario ya está autenticado. Podría agregar un mensaje amistoso que explique por qué los han vuelto a tocar.

Robar
fuente
44
Creo que la mayoría de las personas no tienden a tener más de una identidad para una aplicación web determinada. Si lo hacen, entonces son lo suficientemente inteligentes como para pensar "mi ID actual no tiene mojo, volveré a iniciar sesión como el otro".
Roger Lipscombe
Aunque su otro punto sobre mostrar algo en la página de inicio de sesión es bueno. Gracias.
Roger Lipscombe
4

Desafortunadamente, está lidiando con el comportamiento predeterminado de la autenticación de formularios ASP.NET. Hay una solución alternativa (no lo he probado) discutida aquí:

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

(No es específico de MVC)

Creo que, en la mayoría de los casos, la mejor solución es restringir el acceso a recursos no autorizados antes de que el usuario intente llegar allí. Al eliminar / atenuar el enlace o botón que podría llevarlos a esta página no autorizada.

Probablemente sería bueno tener un parámetro adicional en el atributo para especificar dónde redirigir a un usuario no autorizado. Pero mientras tanto, miro el AuthorizeAttribute como una red de seguridad.

Keltex
fuente
También planeo eliminar el enlace basado en la autorización (vi una pregunta aquí sobre eso en alguna parte), así que codificaré un método de extensión HtmlHelper más adelante.
Roger Lipscombe
1
Todavía tengo que evitar que el usuario vaya directamente a la URL, que es de lo que se trata este atributo. No estoy muy contento con la solución Custom 401 (parece un poco global), así que intentaré modelar mi NotAuthorizedResult en RedirectToRouteResult ...
Roger Lipscombe
0

Intente esto en su controlador Application_EndRequest de su archivo 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
fuente
0

Si usa aspnetcore 2.0, use esto:

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
fuente
0

En mi caso, el problema era "la especificación HTTP utilizaba el código de estado 401 tanto para" no autorizado "como para" no autenticado ". Como dijo ShadowChaser.

Esta solución me funciona:

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
fuente