Integrando oAuth con NancyFx–(i)

Buenas! El objetivo de este post es explicar la solución a la que he llegado para integrar autenticación con oAuth en un sitio web desarrollado con NancyFx. En este primer post veremos como hacerlo “de la forma clásica” pero en otro siguiente nos aprovecharemos de los componentes de autenticación de Katana.

Iré hablando en este blog más sobre OWIN, Katana y también sobre NancyFx, pero por el momento algunas definiciones rápidas:

  • OWIN: Especificación que indica como deben comunicarse los servidores web, el middleware web y las aplicaciones web en .NET.
  • Katana: Implementación de varios componentes (hosts, servidores, middlewares varios) OWIN por parte de Microsoft.
  • NancyFx: Un framework basado en el patrón MVC para desarrollar aplicaciones web y APIs REST. Por decirlo de algún modo NancyFx “compite” con ASP.NET MVC y con WebApi a la vez.

Para saber más de OWIN hay un par de posts en mi blog pero también una serie fenomenal del Maestro. Échales un ojo.

Creando una aplicación Nancy ejecutándose en IIS

Vamos a crear una aplicación web, y vamos a usar NancyFx en lugar de ASP.NET, pero usando IIS (IISExpress).

NancyFx viene en dos “sabores”: puede ser un HttpHandler de ASP.NET (así instalas y configuras NancyFx usando el web.config), pero también existe como componente OWIN, lo que permite su uso dentro de cualquier entorno OWIN.

Para usar NancyFx como HttpHandler de ASP.NET debes instalar el paquete Nancy.Hosting.Aspnet que es el que contiene todas las referencias a ASP.NET (NancyFx es totalmente independiente de ASP.NET). Ahora vamos a crear una aplicación web ASP.NET, así que lo lógico parece ser usar NancyFx como HttpHandler y listos. En muchos casos puede ser lo más lógico, pero no es lo que vamos a hacer. No vamos a usar NancyFx como HttpHandler de ASP.NET. En su lugar lo vamos a utilizar como componente OWIN y, para ello, nos aprovecharemos de un componente de Katana que permite usar componentes OWIN dentro del pipeline de ASP.NET (en terminología OWIN diríamos que este componente de Katana implementa un Host y un servidor Web OWIN). Esto parece dar muchas vueltas, y en muchos casos así puede ser, pero tiene las siguientes ventajas:

  1. Dado que nuestra aplicación será OWIN en cualquier momento podremos abandonar el cómodo regazo de IIS e irnos a cualquier otro host o servidor OWIN.
  2. Incluso aunque no tengamos pensado divorciarnos de IIS, podremos utilizar cualquier componente OWIN que haya por ahí 😉

Como digo, para poder utilizar NancyFx como componente OWIN pero ejecutándose en un pipeline de ASP.NET necesitamos la ayuda de Katana, en concreto del componente Microsoft.Owin.Host.SystemWeb.

Así, los pasos para usar NancyFx como componente OWIN dentro del pipeline de ASP.NET son los siguientes. Antes que nada crea un proyecto de tipo ASP.NET y selecciona el template “Empty”:

image

Luego instala los siguientes paquetes de NuGet:

  • Nancy (el core de NancyFx)
  • Nancy.Owin (Para usar Nancy como módulo OWIN)
  • Microsoft.Owin.Host.SystemWeb (Para usar módulos OWIN en el pipeline de ASP.NET)

Con esto ya tenemos el esqueleto necesario.

El siguiente paso es crear la clase de inicialización de OWIN. Para ello agrega una clase normal y llámala Startup:

Clase de inicializacion OWIN
  1.     public class Startup
  2.     {
  3.         public void Configuration(IAppBuilder app)
  4.         {
  5.             app.UseNancy();
  6.         }
  7.     }

Es importante que la clase se llame Startup. Esto es una convención que sigue Katana (pues quien inicializa los módulos OWIN es Katana a través del paquete Microsoft.Owin.Host.SystemWeb). Si nuestra clase tiene otro nombre recibirás un error como el siguiente:

image

Una solución es o bien renombrar la clase para que se llame Startup o en el caso de que no quieras hacerlo usar el atributo OwinStartupAttribute:

  1. [assembly: OwinStartup(typeof(MyCustomStartup))]

Si ahora ejecutas la aplicación deberías ver algo parecido a lo siguiente:

image

Eso significa que Nancy está funcionando correctamente (este 404 ha sido servido por Nancy). ¡Felicidades! Ya tienes a Nancy corriendo bajo IIS.

Vamos ahora a crear un módulo de Nancy (más o menos equivalente a un controlador de ASP.NET MVC), para gestionar la llamada a la URL / y mostrar una vista con solo el enlace “Login with twitter”. Añade una clase a tu proyecto con el siguiente código:

Nancy MainModule
  1. public class MainModule : NancyModule
  2. {
  3.     public MainModule()
  4.     {
  5.         Get["/"] = _ => View["main.html"];
  6.     }
  7. }

Con esto has creado un módulo de NancyFx y has enrutado todas las peticiones que vayan a la URL “/” para que muestren la vista main.html. Añade pues un archivo main.html (no es necesario que esté en ninguna carpeta Views ni nada) que muestre un enlace a la URL “/authentication/redirect/twitter”.

Si ahora ejecutas el proyecto deberías se tendría que ver el contenido del fichero main.html. Y si pulsas sobre el enlace tienes que recibir el 404 de Nancy.

Perfecto! Ya tenemos el esqueleto base de la aplicación. Ahora vayamos a integrar la autenticación por twitter.

Integrando oAuth con NancyFx a la manera clásica

Nota: El contenido de este apartado presupone que tienes creada una aplicación en twitter y que por lo tanto tienes un consumer key y un consumer secret. También debes configurar la aplicación twitter para que la URL de callback sea /authentication/authenticatecallback">http://localhost:<puerto>/authentication/authenticatecallback

Nota 2: Twitter (y otros proveedores oAuth) no dejan dar de alta aplicaciones cuyo callback sea una dirección de localhost. En este caso lo más rápido es usar el dominio xxx.localtest.me (usa cualquier valor para xxx). El dominio localtest.me está especialmente pensado para estos casos: cualquier subdominio en localtest.me resuelve a 127.0.0.1 😉

NancyFx es un framework muy modular, y no tiene incorporado el concepto de autenticación o autorización. Eso significa que no hay por defecto ninguna API ni nada que nos diga si el usuario está autenticado o bien poder autenticarlo. Todo eso se deja a implementaciones “externas” al core de NancyFx.

P. ej. si queremos autenticar nuestra aplicación basándonos en cookies (lo que en el mundo ASP.NET conocemos como autenticación por forms) debemos instalar el paquete Nancy.Authentication.Forms. Hay otros paquetes para otros tipos de autenticación (como puede ser Nancy.Authentication.Basic para autenticación básica de HTTP o bien Nancy.Authentication.Stateless para basar la autenticación en alguna cabecera específica de la petición). Todos estos paquetes (y más que hay para otros tipos de autenticación) se basan en el modelo de extensibilidad de NancyFx.

Por supuesto hay un paquete para integrarnos con oAuth que antes respondía al interesante nombre de Nancy.Authentication.WorldDomination pero que ahora tiene el aburrido y anodino nombre de Nancy.SimpleAuthentication. Así que añade este paquete a tu proyecto y ya estarás listo para iniciar un flujo para autenticación.

Al añadir este paquete tu web.config se habrá modificado y se habrán añadido las siguientes líneas:

Code Snippet
  1.   <configSections>
  2.     <section name="authenticationProviders" type="SimpleAuthentication.Core.Config.ProviderConfiguration, SimpleAuthentication.Core" />
  3.   </configSections>
  4.   <authenticationProviders>
  5.     <providers>
  6.       <add name="Facebook" key="please-enter-your-real-value" secret="please-enter-your-real-value" />
  7.       <add name="Google" key="please-enter-your-real-value" secret="please-enter-your-real-value" />
  8.       <add name="Twitter" key="please-enter-your-real-value" secret="please-enter-your-real-value" />
  9.       <add name="WindowsLive" key="please-enter-your-real-value" secret="please-enter-your-real-value" />
  10.     </providers>
  11.   </authenticationProviders>

En nuestro caso podemos dejar solo la información del provider de Twitter y debeis rellenar el valor de consumer key y el consumer sectret que os provee Twitter. Acuérdate tambien de mover el tag <configSections> para que sea el primero dentro del <configuration> en el web.config, si no IIS os dará un error!

Ahora el siguiente paso es crear una clase que implemente IAuthenticationCallbackProvider:

Code Snippet
  1. public class MyCustomAuthCallbackProvider : IAuthenticationCallbackProvider
  2. {
  3.     public dynamic Process(NancyModule nancyModule, AuthenticateCallbackData model)
  4.     {
  5.         // En caso de autenticacin OK
  6.     }
  7.  
  8.     public dynamic OnRedirectToAuthenticationProviderError(NancyModule nancyModule, string errorMessage)
  9.     {
  10.         // En caso de error
  11.     }
  12. }

Ejecuta de nuevo tu aplicación. Ahora si pulsas sobre el enlace de login with twitter… ¡deberías ver la página de Twitter!:

image

Una vez el usuario haya dado sus datos y haya autorizado a tu aplicación entonces se ejecuta el método Process de la clase que acabamos de crear. En el parámetro “model” tendrás toda la información necesaria:

image

Una vez sabes cual es el usuario autenticado debes autenticarlo en tu propia web. Es decir Twitter nos ha dado el OK, pero ahora lo que nos falta es utilizar algún mecanismo para autenticar todas las peticiones que realice este usuario en nuestra web.

Nota: Para que todo funcione recuerda que la URL de callback de la aplicación en twitter debe apuntar a /authentication/authenticatecallback y que el enlace de “Login with twitter” debe apuntar a /authentication/redirect/twitter. Esas dos URLs son gestionadas automáticamente por el paquete WorldDomination.

Si queremos utilizar una cookie, tenemos que instalar el paquete Nancy.Authentication.Forms, así… que nada, a por él!

Añadiendo la seguridad por Forms

Una vez tenemos el paquete Nancy.Authentication.Forms añadido ya podemos configurarlo. Son necesarios 3 pasos para que todo funcione correctamente.

El primero es crear una clase que implemente la interfaz IUserIdentity. Esta interfaz es toda la información que el core de Nancy mantiene sobre un usuario autenticado (nombre y permisos):

  1. public class AuthenticatedUser : IUserIdentity
  2. {
  3.     public string UserName { get; set; }
  4.     public IEnumerable<string> Claims { get; set; }
  5. }

El segundo es crear un “User Mapper”. Esto vendría a ser el equivalente (solo lectura) del Membership Provider en ASP.NET. Es decir el encargado de obtener los datos del usuario del repositorio donde se guarden:

  1. public class MyUserMapper : IUserMapper
  2. {
  3.  
  4.     public IUserIdentity GetUserFromIdentifier(Guid identifier, NancyContext context)
  5.     {
  6.         // Obtendriamos el usuario de la BBDD
  7.         return new AuthenticatedUser()
  8.         {
  9.             UserName = "eiximenis",
  10.             Claims = new[] {"Read", "Write", "Admin"}
  11.         };
  12.     }
  13. }

En Nancy todos los usuarios se identifican por un Guid (por supuesto esto no significa que en la BBDD este Guid tenga que ser la PK de la tabla de usuarios!).

El tercer y último paso es indicar al core de Nancy que queremos autenticación por formularios:

  1. public class Bootstrapper : DefaultNancyBootstrapper
  2. {
  3.     protected override void RequestStartup(TinyIoCContainer container, IPipelines pipelines, NancyContext context)
  4.     {
  5.         base.RequestStartup(container, pipelines, context);
  6.         var formsAuthConfiguration = new FormsAuthenticationConfiguration
  7.         {
  8.             DisableRedirect = true,
  9.             UserMapper = new MyUserMapper()
  10.         };
  11.         FormsAuthentication.Enable(pipelines, formsAuthConfiguration);
  12.     }
  13. }

El Bootstrapper es la clase que inicializa todo el core de Nancy. Hasta ahora no teníamos, así que lo añadimos y listos (no hay que indicar en ningún sitio cual es nuestro Bootstrapper, se descubre automáticamente). Con esto ya tenemos la autenticación por forms habilitada en nuestro proyecto.

Volvamos ahora a nuestro CallbackProvider. Lo que haríamos en el método Process es:

  • Consultar la BBDD de usuarios para encontrar el usuario que se corresponda con los datos que nos ha devuelto twitter.
  • En caso de no existir crearlo y obtener su Guid.
  • En caso de existir obtener su Guid.
  • Una vez tenemos el Guid llamar al método LoginAndRedirect. Este método vendría a ser el equivalente al SetAuthCookie de ASP.NET:
  1. public dynamic Process(NancyModule nancyModule, AuthenticateCallbackData model)
  2. {
  3.     // Accederiamos a BBDD para obtener el GUID del usuario
  4.     // O si no lo crearamos
  5.     var userGuid = Guid.NewGuid();
  6.     return nancyModule.LoginAndRedirect(userGuid);
  7. }

¡Y listos! ¡Has terminado!

Añadir contenido securizado

Vamos a añadir un módulo a Nancy que solo se ejecute si el usuario está autenticado. En ASP.NET MVC meterías un [Authorize] en la acción del controlador. En Nancy lo equivalente es usar el module hook Before. Cada modulo de Nancy tiene una propiedad Before en la que puedes poner código que se ejecuta antes de que se ejecute cualquier otro código del módulo (es decir, el código correspondiente a la petición). Desde este código puedes verificar si el usuario está autenticado:

  1. public class SecureModule : NancyModule
  2. {
  3.     public SecureModule()
  4.     {
  5.         Before += ctx =>
  6.         {
  7.             if (ctx.CurrentUser == null)
  8.                 return new Response()
  9.                 {
  10.                     StatusCode = HttpStatusCode.Unauthorized
  11.                 };
  12.             return null;
  13.         };
  14.  
  15.         Get["/Secure"] = _ => View["secure.html",
  16.             new
  17.             {
  18.                 Name = Context.CurrentUser.UserName
  19.             }];
  20.     }
  21. }

Si la propiedad del CurrentUser del contexto es igual a null devolvemos un 401. En caso contrario no hacemos nada y dejamos que siga el pipeline de Nancy (por lo que se ejecutará el código que toque según la URL).

Para terminar añade el archivo secure.html:

  1. <!DOCTYPE html>
  2. <html xmlns="http://www.w3.org/1999/xhtml">
  3. <head>
  4.     <title></title>
  5. </head>
  6. <body>
  7.     Hello @Model.Name
  8. </body>
  9. </html>

(Aunque lo parezca esto NO es Razor. Es el view engine por defecto de Nancy que se llama SSVE (Super Simple View Engine)).

Ya estamos listos para probar! Ejecuta el proyecto y navega a /secure. Deberías ver una página sin nada, pero un vistazo a la pestaña Network de las developer tools nos informa que estamos teniendo un 401:

image

Pefecto. Vuelve a la raíz, y autentícate por twitter. Después del proceso de autenticación volverás a la raiz. Navega a /secure de nuevo y ahora deberías ver la vista secure.html:

image

(Recuerda que todos los usuarios se llamarán eiximenis ya que nuestro IUserMapper siempre devuelve lo mismo ;))

Y hemos finalizado! Ya tienes tu web con Nancy autenticada mediante oAuth. Hemos elegido twitter pero para el resto de proveedores no hay muchas diferencias (el mérito es todo de WorldDomination).

Unas palabras finales

Hemos utilizado Katana para usar NancyFx en “modo OWIN” bajo el pipeline de ASP.NET en IIS. Esto nos permitiría integrarnos con otro middleware OWIN. Así pues, y esta es precisamente la idea de OWIN, podría usar un middleware OWIN para autenticar y autorizar las peticiones. Es decir, no delegar la autenticación y la autorización en el propio NancyFx si no tener un módulo OWIN encargado precisamente de esto…

… en el siguiente post veremos como 😉

Saludos!

PD: Tienes el proyecto completo (VS2013) en http://sdrv.ms/18brcO7 (Archivo NancyKatanaIIS.zip)

El fallo de ASP.NET MVC y el helper Html.DropDownFor

Buenas! Este post es para describir un fallo que he encontrado en el helper Html.DropDownFor y el workaround asociado. Quizá alguien entiende que no es un fallo y quizá es capaz de decirme que razón se esconde bajo este comportamiento… Desde mi punto de vista ninguno, pero bueno… ni tengo (ni pretendo tener) la verdad absoluta.

El problema…

Veamos… Para el helper Html.DropDownFor se usa para crear combos y tiene varias formas de uso (yo mismo escribí un post hace algún tiempo al respecto sobre las combos en ASP.NET MVC). En una de sus formas de uso, podemos mostrar una lista de cervezas y guardar la cerveza seleccionada con el siguiente código:

Clase Beer
  1.     public class Beer
  2.     {
  3.         public int Id { get; set; }
  4.         public string Name { get; set; }
  5.     }

En el controlador tenemos una lista de cervezas (_beers) y un par de acciones para mandar una SelectList con esas cervezas.

Acciones del controlador
  1.         public ActionResult Test()
  2.         {
  3.             ViewBag.Beers = new SelectList(_beers, "Id", "Name", 2);
  4.             return View();
  5.         }
  6.  
  7.         [HttpPost]
  8.         public ActionResult Test(BeerSelectViewModel data)
  9.         {
  10.             ViewBag.Beers = new SelectList(_beers, "Id", "Name", data.SelectedBeerId);
  11.             return View();
  12.         }

La vista y el método que gestionan el POST usan un ViewModel para mantener el ID de la cerveza seleccionada:

ViewModel
  1.     public class BeerSelectViewModel
  2.     {
  3.         public int SelectedBeerId { get; set; }
  4.     }

El código de la vista es sencillo:

Vista Test.cshtml
  1. @model WebApplication3.Controllers.BeerSelectViewModel
  2.  
  3. @using (Html.BeginForm())
  4. {
  5.     @Html.DropDownListFor(m => m.SelectedBeerId, ViewBag.Beers as SelectList)
  6.     <p>
  7.         <input type="submit" class="btn-default" value="submit" />
  8.     </p>
  9. }

Fijaos que usamos el constructor de SelectList que acepta el objeto seleccionado (en este caso el ID de la cerveza seleccionada). La primera vez se usa el 2, de forma que Epidor será la cerveza seleccionada por defecto, la primera vez.

Ahora extendamos a que el usuario pueda seleccionar no una, si no DOS cervezas seleccionadas (con dos combos).

Para ello hacemos los siguientes cambios (que al menos a mi me parecen lógicos). Extendemos el ViewModel para que tenga un array de elementos seleccionados:

Nuevo ViewModel
  1. public class BeerSelectViewModel
  2.   {
  3.       public IEnumerable<int> SelectedBeerIds { get; set; }
  4.   }

En el controlador pasamos en el ViewBag la lista de cervezas (en lugar del SelectList), ya que el SelectList lo construiremos en la vista:

Acciones del controlador
  1. public ActionResult Test()
  2.   {
  3.       ViewBag.Beers = _beers;
  4.       var model = new BeerSelectViewModel()
  5.       {
  6.           SelectedBeerIds = new[] {1, 2}
  7.       };
  8.       return View(model);
  9.   }
  10.  
  11.   [HttpPost]
  12.   public ActionResult Test(BeerSelectViewModel data)
  13.   {
  14.       ViewBag.Beers = _beers;
  15.       return View(data);
  16.   }

En la vista iteramos sobre la propiedad SelectedBeersIds y por cada valor construimos una SelectList cuyo cuarto parámetro (elemento seleccionado) sea el ID por el que estamos iterando:

Vista Test.cshtml
  1. @using WebApplication3.Controllers
  2. @model WebApplication3.Controllers.BeerSelectViewModel
  3. @{
  4.     var beers = ViewBag.Beers as IEnumerable<Beer>;
  5. }
  6.  
  7. @using (Html.BeginForm())
  8. {
  9.     foreach (var id in Model.SelectedBeerIds)
  10.     {
  11.         <p>
  12.             @Html.DropDownListFor(m => m.SelectedBeerIds, new SelectList(beers, "Id", "Name", id))
  13.         </p>
  14.     }
  15.     <p>
  16.         <input type="submit" class="btn-default" value="submit" />
  17.     </p>
  18. }

Recordad eso: Estoy indicando a cada Html.DropDownListFor cual es su elemento seleccionado a través del cuarto parámetro de la SelectList que le asocio. Eso debería funcionar… pero no. NO FUNCIONA. En el código HTML generado ningún tag <option> tiene el atributo selected, así que ambas combos muestran el primer elemento… Ahí está el fallo. Le digo a Html.DropDownFor cual debe ser su elemento seleccionado pero el helper hace caso omiso a esta indicación…

… Y la solución

Después de dar vueltas al asunto, llegué a una solución… Primero lo intenté sin usar el helper Html.DropDownFor y usar tan solo Html.DropDown pero el error era el mismo. Al final la solución que encontré fue usar Html.DropDownFor pero contra otra propiedad del ViewModel. Es decir usar una propiedad (SelectedBeerIds para rellenar el elemento seleccionado de las combos y otra propiedad (SelectedBeerIdsNew) para obtener el valor de vuelta (los nuevos elementos seleccionados). Pero ojo, si desde el el método que gestiona el POST debía devolver de nuevo la vista (p. ej. en el caso de que el ModelState no sea válido) entonces debía hacer lo siguiente:

  • Copiar el valor de la propiedad SelectedBeerIdsNew en SelectedBeerIds
  • Eliminar (poner a null) el valor de SelectedBeerIdsNew
  • Eliminar del ModelState la propiedad SelectedBeerIdsNew.

Si no hacemos las dos últimas cosas las combos no respetarán el elemento seleccionado que les pasamos en el SelectList (si no hacemos la primera nos mostrarán los elementos seleccionados anteriores).

El código en el controlador es:

Acciones Controlador
  1. public ActionResult Test()
  2. {
  3.     ViewBag.Beers = _beers;
  4.     var model = new BeerSelectViewModel()
  5.     {
  6.         SelectedBeerIds = new[] {1, 2}
  7.     };
  8.     return View(model);
  9. }
  10.  
  11. [HttpPost]
  12. public ActionResult Test(BeerSelectViewModel data)
  13. {
  14.     ViewBag.Beers = _beers;
  15.     // En este punto en data.SelectedBeerIdsNew tenemos
  16.     // las nuevas cervezas seleccionadas
  17.     data.SelectedBeerIds = new List<int>(data.SelectedBeerIdsNew);
  18.     data.SelectedBeerIdsNew = null;
  19.     ModelState.Remove("SelectedBeerIdsNew");
  20.     return View(data);
  21. }

Fíjate en el código necesario en el método que gestiona el POST. Si no establecemos SelectedBeerIdsNew a null y no eliminamos la clave SelectedBeerIdsNew del ModelState no funciona.

El resto de código es igual excepto que en la vista el Html.DropDownFor es para la propiedad SelectedBeerIdsNew (aunque iteramos sobre SelectedBeerIds):

Codigo de la vista
  1. @using WebApplication3.Controllers
  2. @model WebApplication3.Controllers.BeerSelectViewModel
  3. @{
  4.     var beers = ViewBag.Beers as IEnumerable<Beer>;
  5. }
  6.  
  7. @using (Html.BeginForm())
  8. {
  9.     foreach (var id in Model.SelectedBeerIds)
  10.     {
  11.         <p>
  12.             @Html.DropDownListFor(m => m.SelectedBeerIdsNew, new SelectList(beers, "Id", "Name", id))
  13.         </p>
  14.     }
  15.     <p>
  16.         <input type="submit" class="btn-default" value="submit" />
  17.     </p>
  18. }

Y esto es mas o menos todo… En mi opinión es un bug, porque insisto: en todo momento uso la sobrecarga de SelectList que le indica el elemento seleccionado. Si no la usase entendería el comportamiento (hasta sería lógico), pero la estoy usando. No entiendo porque no hace caso de lo que le indica el SelectList en este caso.

¿Qué opináis vosotros?

Saludos!