ASP.NET MVC, [Authorize] y jQuery.load

Muy buenas! Estreno el blog este 2014… dios a finales de Febrero! A ver, si empezamos a retomar el ritmo…

Este es un post sencillito, por si os encontráis con ello. La situación es la siguiente: Tenéis controladores que devuelven vistas parciales, las cuales desde JavaScript incluís dentro de vuestro DOM a través de una llamada Ajax, usando p. ej. el método load de jQuery.

Todo funciona correctamente, hasta que un día el usuario entra en el site, se va a comer y cuando vuelve pulsa uno de esos enlaces (o botones o lo que sea) que incluyen una de esas llamadas Ajax… Y ocurre que en lugar de aparecer la vista parcial, aparece la página de Login allí incrustada.

La razón, obviamente, es que la acción que devuelve la vista parcial está protegida con [Authorize] y al haber caducado la cookie de autorización, este atributo manda un HTTP 401 (no autorizado). Hasta ahí bien. Entonces entra en juego nuestro amigo FormsAuthentication, que “captura” este 401 y lo convierte en un 302 (redirección) que es lo que recibe el navegador. Desde el punto de vista del navegador, lo que llega es un 302 por lo que este, obendientemente, se redirige a la página de Login. Las peticiones Ajax hacen caso del HTTP 302 y por lo tanto el resultado de la redirección (la página de Login) se muestra.

Una alternativa sencilla y rápida para solucionar esto consiste en modificar la petición modificada por FormsAuthentication, de forma que cambiamos todos los 302 que sean resultado de una petición Ajax por un 401 y así revertir lo que FormsAuthentication hace.

  1. protected void Application_EndRequest()
  2. {
  3.     var context = new HttpContextWrapper(this.Context);
  4.     if (FormsAuthentication.IsEnabled && context.Response.StatusCode == 302
  5.         && context.Request.IsAjaxRequest())
  6.     {
  7.         context.Response.Clear();
  8.         context.Response.StatusCode = 401;
  9.     }
  10. }

Con esto convertimos todos los 302 en 401 cuando sean peticiones Ajax y estemos bajo FormsAuthentication. Ojo, que los convertimos todos, incluso aquellos 302 legítimos que podrían haber.

Ahora ya solo queda actualizar nuestro código JavaScript y comprobar que no recibimos un 402 😉

Postdata 1: .NET Framework 4.5

Si usas .NET Framework 4.5 (VS2012), ya no es necesario que hagas este truco. En su lugar puedes usar la propiedad SuppressFormsAuthenticationRedirect de HttpResponse y ponerla a true. Si el valor de esa propiedad es true, pues FormsAuthentication no convierte los 401 en 302.

Es una propiedad que debes establecer cada vez, por lo que lo puedes hacer de nuevo en el Application_EndRequest de Global.asax si lo deseas.

Si alguien me pregunta porque narices esa propiedad es a nivel de Response (ya me dirás tu porque el objeto Response tiene que “entender” del framework de autorización), pues no lo sé… pero no me termina de gustar, la verdad.

Postdata 2: Katana Cookie Middleware

Ya lo decía el bueno de Andrés Montes: La vida puede ser maravillosa. Si en FormsAuthentication arreglaron esta situación con la propiedad SuppressFormsAuthenticationRequest en el middleware de autenticación por cookies de Katana volvemos a la situación anterior. Y si usas VS2013 o los nuevos templates de ASP.NET en VS2012 no estarás usando FormsAuthentication si no el middleware de Katana.

Por suerte Katana está mejor pensado que FormsAuthentication y podemos configurar mucho mejor el middleware de autenticación basada en cookies.

Buscad donde se configura el middleware basado en cookies de Katana, que por defecto es en el fichero App_Start/StartupAuth.cs y sustutís:

  1. app.UseCookieAuthentication(new CookieAuthenticationOptions
  2. {
  3.     AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  4.     LoginPath = new PathString("/Account/Login")
  5. });

por:

  1. app.UseCookieAuthentication(new CookieAuthenticationOptions
  2. {
  3.     AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
  4.     LoginPath = new PathString("/Account/Login"),
  5.     Provider = new CookieAuthenticationProvider
  6.     {
  7.         OnApplyRedirect = ctx =>
  8.         {
  9.             if (!IsAjaxRequest(ctx.Request))
  10.             {
  11.                 ctx.Response.Redirect(ctx.RedirectUri);
  12.             }
  13.         }
  14.     }
  15. });

De esta manera tomamos el control del redirect por 401 y lo hacemos solo si la request no es Ajax. Bueno, bonito y barato.

Ah si! El método IsAjaxResponse… Este método no es el método IsAjaxResponse clásico (ctx.Request es una IOwinRequest) así que os lo tendréis que crear vosotros. Aquí os pongo una implementación:

  1. public static bool IsAjaxRequest(IOwinRequest request)
  2. {
  3.     IReadableStringCollection query = request.Query;
  4.     if (query != null)
  5.     {
  6.         if (query["X-Requested-With"] == "XMLHttpRequest")
  7.         {
  8.             return true;
  9.         }
  10.     }
  11.  
  12.     IHeaderDictionary headers = request.Headers;
  13.     if (headers != null)
  14.     {
  15.         if (headers["X-Requested-With"] == "XMLHttpRequest")
  16.         {
  17.             return true;
  18.         }
  19.     }
  20.     return false;
  21. }

No,  no me deis las gracias… Si el método IsAjaxRequest os funciona las dais al equipo de Katana, ya que está copiado del código fuente de Katana (concretamente de aquí). Si, si… yo también me pregunto porque es privado este método y no un método de extensión público.

En fin… eso es todo! Espero que os sea útil!

Saludos!

Fuentes usadas:

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *