AllowAnonymous en ASP.NET MVC 4

 

ASP.NET MVCDesde el principio de los tiempos, ASP.NET MVC dispone de un mecanismo muy sencillo para controlar el acceso a acciones, basado en los sistemas de autenticación por formulario estándar de ASP.NET.

A grandes rasgos, el asunto consiste en decorar acciones, o incluso los controladores completos, con el atributo [Authorize], de forma que si el usuario no ha superado el procedimiento de autenticación, no se podrá acceder a ellas. Además, gracias a los parámetros admitidos por este filtro, es posible indicar qué usuarios concretos pueden ejecutar la acción, o qué roles son los permitidos:

public class CustomersController : Controller
{
    [Authorize(Roles = "manager")]
    public ActionResult Delete(int id)
    {
        // TODO: Delete a customer
    }

    //...
}

Sin embargo, había algunos escenarios que eran algo molestos de implementar usando esta técnica. Imaginad que estáis desarrollando una web totalmente privada, en la que es necesario incluir el atributo [Authorize] a todos los controladores: el ideal sería utilizar filtros globales, ¿no? En vez de modificar cada uno de los controladores, haríamos lo siguiente en el global.asax.cs:

    public static void RegisterGlobalFilters(GlobalFilterCollection filters)
    {
        filters.Add(new HandleErrorAttribute());
        filters.Add(new AuthorizeAttribute());
    }

Y como casi todo en la vida, no es tan sencillo. Este atributo afectaría a las acciones que retornan las vistas del propio formulario de login, por lo que simplemente no sería posible acceder a la aplicación. Lógicamente hay varias soluciones para este problema; por ejemplo, podríamos introducir a mano el filtro en los controladores, o incluso mejor aún, crear un controlador base con el atributo y heredar de él todos los controladores menos los destinados a facilitar la identificación del usuario.

ASP.NET MVC 4 introduce un nuevo filtro, denominado [AllowAnonymous] cuya utilidad seguro que podéis deducir en este momento: aplicado a un controlador o acción, hace que se ignore en éste cualquier posible atributo [Authorize] que pudiera haberse aplicado al mismo. De hecho, en la misma plantilla de proyectos MVC 4 ya vemos que se utiliza bastante en el controlador AccountController , tradicionalmente destinado a realizar las tareas de conexión, registro y desconexión de usuarios:

    [Authorize]
    public class AccountController : Controller
    {

        //
        // GET: /Account/Login

        [AllowAnonymous]
        public ActionResult Login()
        {
            // ... 
        }
    }

Y este post podría acabar aquí, pero hay una curiosidad que quería comentar sobre la forma en que está implementado este filtro. Si observamos su código fuente, vemos que es, poco más o menos, así:

namespace System.Web.Mvc 
{  
   [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, 
                   AllowMultiple = false, Inherited = true)] 

   public sealed class AllowAnonymousAttribute : Attribute 
   { 

   } 
}

Salvo la metainformación contenida en el AttributeUsage, el resto es una clase vacía (!). Aunque al principio puede parecer raro, es muy lógico: la magia la pone el atributo [Authorize] en cuya implementación encontramos la lógica que posibilitará la ejecución de una acción si se encuentra decorada (ella o su controlador) con [AllowAnonymous], saltándose todas las comprobaciones de autorización:

bool skipAuthorization = 
    context.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true)
    || context.ActionDescriptor.ControllerDescriptor.IsDefined(
                typeof(AllowAnonymousAttribute), inherit: true);

if (skipAuthorization)
{
    return; 
}

Publicado en Variable not found.

Login único para subdominios en ASP.NET

 ASP.NETImaginad que tenemos un sistema web de cierto volumen y decidimos estructurarlo en aplicaciones independientes, cada una publicada en un subdominio propio:

  • www.acme.org, que sería el sitio principal.
  • crm.acme.org, con el sistema CRM de la empresa.
  • erp.acme.org, con un sistema de gestión empresarial.
  • administration.acme.org con las herramientas de administración del sistema.
  • etc.

Desde un punto de vista operativo, es probable que nos interese suministrar un mecanismo de autenticación de usuarios compartido entre todas estas aplicaciones, de forma que el usuario, una vez identificado, pueda pasar de una a otra sin necesidad de introducir de nuevo sus credenciales.

No es una tarea complicada en ASP.NET, aunque hay que hacer algunos ajustillos para que todo funcione correctamente. Veámoslos.

Primero: ampliar el alcance de la cookie

Como sabemos, el procedimiento estándar de autenticación consiste en comprobar que las credenciales suministradas por un usuario son correctas (con membership o cualquier otro mecanismo), y en caso afirmativo, generar una cookie con información encriptada sobre el mismo. Esta cookie, llamada por defecto “.ASPXAUTH”, viaja en las sucesivas peticiones hacia el servidor, de forma que éste puede comprobar que el usuario ha sido autenticado satisfactoriamente con anterioridad.

El problema es que por defecto esta cookie es específica para cada host, por lo que sólo estará disponible para el dominio desde el que ha sido generada, con todos sus subdirectorios. Así, cuando se supera el procedimiento de autenticación y se crea la cookie (por ejemplo llamando a FormsAuthentication.SetAuthCookie()), lo que se enviará al servidor en los encabezados de la respuesta es lo siguiente:

Set-Cookie: .ASPXAUTH=F1E37685DF9CBED74094D02958BA239B4AEAA0BDEF2FF379A2E2C5A
4B7F9AC271B7F14BCFFE3E18799434EE8886CF4A0227E6BE92BC91E
34601D0FCC18D3F1786D5060329DB578DF2BB5148F6AB2972D72C3D
B17A437CE977660E552B92A6E5F981F3E6CE6037065244E1F0AB0BD
A570D61DEB02; path=/; HttpOnly

Pues bien, para conseguir nuestros objetivos, lo primero que nos interesa es asegurar que esta cookie estará disponible en los subdominios del dominio desde la cual se ha generado.

Afortunadamente podemos configurar bastantes aspectos del sistema de autenticación basado en formularios simplemente tocando un poco el web.config, y en este caso simplemente debemos indicar el dominio (de segundo nivel) para el cual queremos que la cookie sea válida, incluyendo sus subdominios. en el parámetro domain:

  <authentication mode="Forms">
    <forms loginUrl="~/Account/LogOn" timeout="2880" domain="acme.org" />      
  </authentication>

El valor de este parámetro será el nombre del dominio raíz del que colgarán todos los subdominios a los que haremos la cookie visible. Una vez introducido este valor en el web.config, la cookie de autorización será la siguiente:

Set-Cookie: .ASPXAUTH=4A67B460978D78217D52248318EB68717E857D7C08012057D0B6731
896E0E4EB9023AD2C02024CFB7D190617CCCD97FBA1E6ED484CE3FF
3581E7C8BE31FA204508F5ABB0DF994ADD698369132A5AF932AFF40
A267422C9ABCA86E620B9041ABB2E97C516880960F3D8193B209616
5978108AB500; domain=acme.org; path=/; HttpOnly

Este cambio tendremos que hacerlo únicamente en la aplicación que genere la cookie, es decir, aquella que incluya la implementación del procedimiento de autenticación del usuario. En nuestro caso, por ejemplo, podría ser la aplicación “raíz”, www.acme.org.

Segundo: encriptar la cookie usando una clave común

Esa secuencia de letras y números que veis en la cookie no es más que el resultado de encriptar la información que necesita ASP.NET para mantener información sobre el usuario autenticado. Esta encriptación se realiza utilizando un algoritmo y unas claves específicas para cada sitio web, que por defecto son generadas de forma automática.

Así, si queremos que distintos sitios web puedan desencriptar la cookie y acceder a su contenido, lo cual es fundamental para mantener el usuario conectado, debemos hacer que todos ellos compartan el algoritmo y clave de encriptación. Esto se hace desde el web.config estableciendo estos aspectos en la entrada <machineKey>:

...
< system.web>
<machineKey <!-- ¡Ojo no copiar y pegar! -->
validationKey="6171035B16CD1EE0E401BA3E7348DE49FA9FB9C043B3CDE3BA4FF
EDDC84B167C68B83916FAD8AEE4CFEE001AD5CEA8A4B3E28D51F9
D5EA55CD5F276E67B71FC6"
decryptionKey="3F12339F897F687F4456FEC2446167C621BDE5F664178CBEF9AF0
40DB82EC806"
validation="SHA1"
decryption="AES"
/>
...
< /system.web>

Puedes generar este elemento usando el generador de MachineKey online, copiar el código y pegarlo en la sección <system.web> del web.config de todos los sitios web, raíz y subdominios, de la aplicación.

A partir de este momento ya podemos probar el sistema completo en funcionamiento. Si el mecanismo de autenticación se encuentra en www.acme.org, la autorización viajará al navegar por todos sus subdominios (crm.acme.org, erp.acme.org…), éstos serán capaces de desencriptar las credenciales y, por tanto, el usuario permanecerá logado en el sistema.

Tercero: ajustar Redirecciones

Dado que hemos considerado que todas las aplicaciones son privadas, con toda seguridad estarán protegidas contra accesos de usuarios no autenticados.Por ejemplo, en ASP.NET MVC probablemente tengamos todas las acciones protegidas con un filtro [Authorize], o en WebForms tendremos secciones <authorization> en el web.config para denegar el acceso a todas sus funcionalidades.

En cualquier caso, nos interesa que los usuarios no autenticados sean redirigidos a la aplicación desde la cual pueda autenticarse, por lo que podemos debemos indicar la URL de la página de login en el parámetro loginUrl como sigue:

    <authentication mode="Forms">
      <forms loginUrl="http://www.acme.org/account/logon" timeout="2880">
      </forms>
    </authentication>

Este cambio sería necesario hacerlo en todas las aplicaciones a las que no vamos a permitir el acceso anónimo (crm.acme.org, erp.acme.org, …), con lo que aseguramos que cualquier intento de entrada de usuarios no autenticados será redirigida al sitio principal, desde el cual podrá identificarse.

Cuarto: retocar la URL de retorno y procesarla tras la autenticación

Casi hemos acabado, pero aún hay un detalle cuya solución no es tan inmediata como los puntos anteriores.
El parámetro ReturnUrl
Cuando se produce la redirección hacia la página de login, automáticamente se añade a la petición un parámetro llamado ReturnUrl donde se almacena la URL a la que estaba intentado acceder el usuario. Esto permite devolverlo a ella una vez se haya autenticado en el sistema.

El problema es que este parámetro no incluye el host, por lo que en una aplicación distribuida en distintos dominios esta información se perderá. Es decir, si un usuario intenta acceder directamente a erp.acme.org/customers/index, será redirigido a la URL http://www.acme.org/account/logon?ReturnUrl=customers/index para ser autenticado, y cuando esto ocurra, el sistema no tiene información suficiente como para devolverlo a la página a la que deseaba ir en un principio (en erp.acme.org).

Lamentablemente no podemos “influir” en la forma en que ASP.NET genera el contenido para este parámetro al redirigir al usuario, por lo que o bien hacemos nosotros la redirección de forma manual al detectad que el usuario no se ha autenticado, o bien nos introducimos en algún punto avanzado del ciclo de ejecución de la petición para modificar el valor original del parámetro.

Un primer acercamiento de la implementación de esta última opción podría ser la siguiente, implementando el evento Application_EndRequest (en el global.asax.cs, válido tanto en MVC como en Webforms):

protected void Application_EndRequest(object sender, EventArgs e)
{
    if (Response.StatusCode == (int)HttpStatusCode.Found && !Request.IsAuthenticated)
    {
        string redirectUrl = this.Response.RedirectLocation;
        if (redirectUrl.Contains("ReturnUrl="))
        {
            var host = HttpUtility.UrlEncode(
                            "http://" +
                            this.Request.Url.Host +
                            (Request.Url.IsDefaultPort ? "" : ":" + Request.Url.Port));
            Response.RedirectLocation = redirectUrl.Replace("ReturnUrl=", "ReturnUrl=" + host);
        }
    }
}

Como podéis observar, simplemente intentamos detectar cuándo la respuesta enviada al cliente es una redirección, y si el usuario no está autenticado y existe un parámetro ReturnUrl, lo modificamos para que presente también el host (y puerto). De esta forma, ya llegará a la página de login la dirección completa a la que hay que enviar el usuario cuando supere la autenticación.

Ya llega la URL completa!

Y por último, ya lo único que quedaría sería implementar la redirección a la URL suministrada en el parámetro ReturnUrl una vez confirmada la identidad del usuario, es decir, tras establecer la cookie.

Eso sí, tened un poco de cuidado antes de redirigir y comprobad que la dirección a la que vais a enviarlo forma parte de vuestro sitio (por ejemplo, comprobando que el host de destino pertenece a acme.org) para evitar la vulnerabilidad Open Redirect.

Publicado en Variable not found.

ASP.NET MVC 4 Release Candidate ya disponible

ASP.NET MVC¡Bueno, pues parece que esto se mueve! Hace unos días ha sido publicada la Release Candidate de ASP.NET MVC 4 coincidiendo con la liberación de Windows 8 Release Preview y Visual Studio 2012 Release Candidate (que, de hecho, incluye de serie MVC 4 RC).

Y como viene siendo costumbre, vamos a dar un repaso a todo lo que encontramos en esta nueva entrega, que presumiblemente será la última (bueno, o penúltima, nunca se sabe) antes de la versión definitiva. Eso sí, para no hacer el post demasiado largo nos centraremos exclusivamente en los cambios más destacables y visibles que se han introducido respecto a la versión beta.

1. Cambios en plantillas de proyecto

Plantillas de proyecto en MVC 4En la versión RC de MVC 4, al crear un nuevo proyecto encontramos plantillas ya conocidas de versiones y revisiones anteriores del framework:

  • Empty
  • Internet Application
  • Intranet Application
  • Mobile Application
  • Web API

Adicionalmente, aparece una nueva plantilla, llamada Basic, que podríamos decir que es un término medio entre la plantilla vacía y la aplicación para internet. En ella se incluyen sólo unas vistas shared (el archivo _viewstart, un _layout y la página estándar de error), los scripts, recursos gráficos y estilos utilizados en ellas, y la inicialización de bundles, rutas y filtros. Nada de los célebres HomeController o AccountController y sus vistas asociadas.

Otra cosa curiosa: recordaréis que la versión Beta traía “soporte experimental” para Single Page Applications, o Aplicaciones de Página Única. Pues bien, se ve que ahora han optado por no continuar con este experimento y en la RC de MVC 4 han eliminado la plantilla para proyectos SPA, que seguirá evolucionando pero de forma independiente a MVC 4. Casi que mejor así 😉

También ha sido eliminado de la plantilla Internet Application el sistema de autenticación basado en Ajax que quedaba tan molón, pero que complicaba bastante el código del AccountController.

2. Nueva convención de ubicación de archivos (App_Start)

El código de inicialización de las aplicaciones cada vez va tomando más volumen; incluso en las aplicaciones más simples nos encontramos con la necesidad de definir rutas, bundles, filtros globales, inicialización del acceso a datos, mapeos para inyección de dependencias, y un largo etcétera.

imageIntroducir todo este código en el global.asax.cs no es una buena idea, y como el framework no aportaba ninguna sugerencia al respecto, al final acabábamos poniéndolo cada uno donde nos parecía oportuno.

Pues bien, a partir de hora, todo el código de inicialización de las aplicaciones se encontrará por defecto en la carpeta App_Start. De hecho, en las plantillas no vacías ya vemos cómo se incluye la definición de rutas, filtros o bundling. Por otra parte, en el global.asax.cs sólo encontraremos las correspondientes llamadas a dada uno de estos inicializadores.

3. Nuevas referencias

Examinando las referencias de los distintos tipos de proyecto encontramos nuevos ensamblados, que corresponden con los siguientes componentes:

  • Json.NET (en el ensamblado Newtonsoft.json) que es ahora utilizado como formateador JSON por WebAPI, sustituyendo las funcionalidades proporcionadas tradicionalmente por System.Json.dll.
  • WebGrease, que es el nuevo sistema de optimización de scripts, css e incluso imágenes, que es ya utilizado por el sistema de bundling del paquete System.Web.Optimization.
  • Antl3.Runtime es la adaptación para .NET del runtime del analizador de gramáticas ANTLR (Another Tool for Language Recognition). Es una dependencia de WebGrease.
  • Entity Framework 5.0 RC, que aporta toda la infraestructura de acceso a datos que necesitamos para nuestras aplicaciones. Esta revisión incorpora múltiples novedades: tipos enumerados, tipos espaciales, TVFs, mejoras en el diseñador (VS2012), etc. Puedes leer más sobre las novedades incorporadas en EF 5 RC aquí.

4. Avances en WebAPI

WebAPI, el nuevo framework que tanto entusiasmo está provocando, sigue evolucionando y mejorando con esta versión. De hecho, diría que es uno de los puntos que más cambios han sufrido, al menos visiblemente, desde su aparición en la versión beta de ASP.NET MVC 4. Las novedades de mayor calado son las siguientes:

  • Los métodos de nuestro API que retornen un IQueryable<T> deben decorarse con el atributo [Queryable] para que esté permitido componer consultas sobre ellos.
  • Es posible registrar de forma global una clase que implemente ITraceWriter para utilizarla como punto único de trazado y diagnóstico, en la que podemos registrar los eventos que vayan produciéndose en el sistema.
  • También es posible, usando el método extensor RegisterForDispose(), asociar a un Request un objeto IDisposable, de forma que será liberado automáticamente cuando termine de ser procesada la petición. Muy interesante para gestionar de forma correcta el ciclo de vida de los objetos.
  • Podemos utilizar el servicio ApiExplorer de nuestros Api Controllers para obtener metainformación en tiempo de ejecución sobre los servicios, lo que da opción para crear sistemas de ayuda o clientes dinámicos (por ejemplo para pruebas).
  • Ha sido simplificada la forma de solicitar y responder objetos con negociación de contenidos. Ahora, por ejemplo, usaremos el método extensor CreateHttpResponse<T> para retornar objetos desde nuestros métodos con control total sobre la respuesta. Las clases que usábamos antes, HttpRequestMessage<T> y HttpResponseMessage<T>, han sido eliminadas.
  • Se han introducido métodos específicos en la propiedad Headers para obtener las cookies asociadas a una petición y para añadirlas a una respuesta.
  • Y muchos otros cambios internos que podéis consultar en el documento de notas de la revisión.

La verdad es que algunos de los avances que han sido incorporados tienen también sentido para controladores MVC convencionales y no sólo para WebAPI. Espero que estas dos líneas paralelas serán unificadas vistas a la versión definitiva, porque la verdad es que no le veo demasiado sentido a mantenerlas separadas. De hecho, en estos momentos resulta un poco caótico que existan clases duplicadas en ensamblados distintos (System.Web.Http para WebAPI y System.Web.Mvc para ASP.NET MVC).

Por poner un ejemplo, el filtro HttpGet podemos encontrarlo en ambos namespaces, y dependiendo del contexto en que lo necesitemos (WebAPI o MVC) debemos usar uno u otro, y lo mismo ocurre con muchos otros elementos, tanto internos como externos. Esto puede provocar muchos fallos difíciles de detectar dado que las clases usadas dependerán en muchos casos del namespace usado en nuestro código.

En fin, espero que esto quede unificado en algún momento…

5. Cambios en el sistema de bundling

Hasta ahora, el mismo sistema de bundling venía de serie con lógica para incluir de forma automática los scripts de uso más habitual en aplicaciones web usando métodos como EnableDefaultBundles(). Sin embargo, los desarrolladores no podíamos modificarla porque se incluía en los propios componentes de la biblioteca de optimización.

A partir de la RC, la configuración de los bundles por defecto se realiza de forma explícita en nuestra aplicación. En la nueva carpeta /App_Start encontramos la clase BundleConfig, en cuyo método estático RegisterBundles() se encarga de ello, y obviamente podemos modificarlo a nuestro antojo:

public class BundleConfig
{
    public static void RegisterBundles(BundleCollection bundles)
    {
        bundles.Add(new ScriptBundle("~/bundles/jquery").Include("~/Scripts/jquery-1.*"));
 
        bundles.Add(new ScriptBundle("~/bundles/jqueryui").Include("~/Scripts/jquery-ui*"));
 
        bundles.Add(new StyleBundle("~/Content/css").Include("~/Content/site.css"));
 
        [...]

Respecto a revisiones anteriores de MVC 4, se ha simplificado un poco la sintaxis para crear bundles, asociarlos al componente encargado de realizar su transformación, y especificar los recursos a incluir en el paquete.

Por otro lado, las referencias desde las vistas a los distintos bundles también se han simplificado y se generan de forma automática utilizando clases estáticas provistas desde el ensamblado System.Web.Optimization:

        @Styles.Render("~/Content/css")
        @Scripts.Render("~/bundles/jquery")

Otro aspecto muy interesante es que dependiendo de si estamos ejecutando la aplicación en modo release o debug los bundles serán compactados y minimizados o no, para facilitar la depuración.

6. Cambios en scaffolding

También se han introducido cambios relativos a la creación de controladores:

  • Ahora es posible añadir un controlador sobre cualquier carpeta del proyecto pulsando el botón derecho sobre ella. Esto facilitará la organización de aplicaciones donde convivan controladores MVC y controladores WebAPI.
  • Se ha eliminado la plantilla de controlador Ajax grid que encontrábamos en la beta del producto. El motivo, según indican en el documento de notas de la revisión, no es otro que dejar al mínimo la lista de plantillas disponibles.
  • Se ha creado una nueva plantilla para controladores WebAPI basados en una entidad y contexto de datos de Entity Framework.

Por otra parte, las famosas recetas (recipes) han sido finalmente eliminadas de MVC 4 y serán incorporadas como parte de Nuget. De hecho, si lo pensamos un poco, la idea inicial tiene bastante más que ver con Nuget que con MVC, así la decisión creo que es bastante correcta.

Y hasta aquí llegamos. Tenemos más detalles descritos en el documento de notas de la revisión, que os recomiendo que leáis, y seguro iremos descubriendo muchos otras novedades durante las próximas semanas.

Enlaces:

Publicado en Variable not found.