Cambiar el esquema con ASP.NET Identity

Una de las grandes novedades (probablemente la de mayor calado si obviamos la revolución de OWIN) de la nueva versión de ASP.NET es ASP.NET Identity, que sustituye al viejuno Membership (que apareció allá en 2005). ASP.NET Identity está diseñado para dar solución a muchos de los problemas de los que Membsership acaecía.

Una de las preguntas más recurrentes en los foros de la MSDN y fuera de ellos es como usar Membership con un esquema de base de datos propio. Esto básicamente implica crearte un custom memberhip provider lo que, sin ser excesivamente complicado, te hace bueno… derivar de una clase con un porrón de métodos abstractos, la mitad de los cuales puede que ni apliquen en tu caso y que vas a dejar llenos de NotImplementedException. Pesado, feo y además un NotImplementedException siempre demuestra un fallo de diseño en algún punto del sistema (en este caso en el Membership).

ASP.NET Identity ha sido diseñado, entre otras cosas, para solucionar este problema: que los cambios de esquema de la BBDD no impliquen tantos problemas. En este post veremos como adaptar ASP.NET Identity a un esquema de BBDD propio, utilizando, eso sí, el proveedor de ASP.NET Identity para Entity Framework. Y es que otras de las características de ASP.NET Identity es que puede trabajar con varios proveedores de datos (p. ej. se podría hacer un proveedor noSQL para MongoDb) aunque, obviamente (no le pidamos peras al olmo) implementar un proveedor nuevo no es algo trivial. MS ha implementado uno para EF que es el que mucha gente va a utilizar, así que centrémonos en él. Dicho proveedor reside en el paquete Microsoft.AspNet.Identity.EntityFramework y se incorpora por defecto cuando seleccionamos la opción “Individual User Accounts” al crear un proyecto ASP.NET en VS2013.

Si no hacemos nada, el esquema por defecto que se nos crea es el siguiente:

image

Miremos ahora alguna de las tablas, p. ej. AspNetUsers:

image

Bien, veamos ahora como podemos añadir un campo adicional a esta tabla.

Añadiendo un campo adicional a la tabla de usuarios

Honestamente este es la parte fácil (y la que encontrarás en la mayoría de tutoriales sobre ASP.NET Identity). Pero antes de ver como hacerlo déjame contarte un par de cosillas sobre como está montado el proveedor de ASP.NET Identity para EF. Dicho proveedor se basa en EF Code First y en varias clases que implementan las interfaces necesarias para ASP.NET Identity. P. ej. ASP.NET Identity trabaja con la interfaz IUser y el módulo de EF utiliza la clase IdentityUser. La interfaz IUser es muy simple, solo define dos campos (Id y UserName ambos de tipo string). La clase IdentityUser implementa dicha interfaz y añade varios campos más (como PasswordHash y SecurityStamp). Podríamos decir que la tabla AspNetUsers se genera a partir de dicha clase.

Nosotros no podemos modificar dicha clase y añadir campos, así que el proveedor de ASP.NET Identity para EF ha optado por ofrecernos otro mecanismo. Dicho proveedor para trabajar necesita un contexto de EF (es decir un DbContext), pero dicho DbContext debe derivar de IdentityDbContext<T> donde el tipo T derive de IdentityUser. Así pues para añadir un campo simplemente debemos:

  1. Crear una clase X derivada de IdentityUser y añadir el campo necesario.
  2. Derivar de IdentityDbContext<X> donde X es la clase creada en el punto anterior.

El template de VS2013 nos crea una clase llamada ApplicationUser que ya hereda de IdentityUser. Dicha clase está vacía y está pensada para que nosotros añadamos los campos necesarios. También nos crea un contexto de EF (llamado ApplicationDbContext) que hereda de IdentityDbContext<ApplicationUser>. Es decir, nos da casi todo el trabajo hecho 🙂

Para no ser originales, vamos a hacer el ejemplo clásico que encontrarás en cualquier post: añadir la fecha de nacimiento. Para ello nos basta con añadir dicho campo en la clase ApplicationUser:

  1. public class ApplicationUser : IdentityUser
  2. {
  3.     public DateTime? BirthDay { get; set; }
  4. }

Si ahora ejecutas de nuevo la aplicación, seguramente te dará el siguiente error: The model backing the ‘ApplicationDbContext’ context has changed since the database was created. Consider using Code First Migrations to update the database (http://go.microsoft.com/fwlink/?LinkId=238269)

Esto es porque se ha modificado el esquema de datos y ahora tenemos un código que no se corresponde a la BBDD. Tenemos dos opciones: o eliminar la BBDD (y perder todos los datos) o usar Migrations. Para usar Migrations tenemos que habilitarlas primero mediante el Package Manager Console, tecleando Enable-Migrations. Una vez las tengas habilitadas debemos:

  1. Añadir una migración que refleje el cambio realizado en ApplicationUser, usando Add-Migration <nombre_migracion> en el Package Manager Console
  2. Actualizar la BBDD usando Update-Database en el Package Manager Console.

En mi caso cuando he tecleado Add-Migration AddBirthday me ha generado dentro de una carpeta Migrations el archivo de migración. Dicho archivo es una clase C# que le indica a Migrations que hacer. Se genera automáticamente “comparando” el esquema de la BBDD con el esquema que tendría que haber según la clases de EF code first. Para que veas, en mi caso la clase de migración contiene el siguiente código:

  1. public partial class AddBirthday : DbMigration
  2. {
  3.     public override void Up()
  4.     {
  5.         AddColumn("dbo.AspNetUsers", "BirthDay", c => c.DateTime());
  6.     }
  7.         
  8.     public override void Down()
  9.     {
  10.         DropColumn("dbo.AspNetUsers", "BirthDay");
  11.     }
  12. }

Puedes ver que lo que hará es añadir la columna BirthDay. Insisto: dicha clase se genera automáticamente al hacer Add-Migration. Cuando ejecutes Update-Database, se aplicarán los cambios indicados.

Así que bueno… lo único que te queda es esto, ejecutar el comando Update-Database desde el Package Manager Console. Cuando lo apliques verás algo parecido a esto:

image

Listos… Si ahora miras el esquema de la tabla AspNetUsers verás que tenemos ya el nuevo campo:

image

Fácil y sencillo. La verdad, comparado con lo que se tenía que hacer para conseguir lo mismo con el Membership es una maravilla 😛

Cambiar el esquema

Vale… añadir campos es sencillo (hemos visto el ejemplo clásico que es la tabla de usuarios, para el resto de tablas es lo mismo pero con otras clases). Pero… y cambiar el esquema? Si quiero que la tabla AspNetUsers se llame simplemente Users y que PasswordHash se llame Pwd p. ej.? Que tenemos que hacer?

Pues bien, eso también es posible gracias al uso de EF Code First. En este caso vamos a tener que tocar el contexto de EF (la clase ApplicationDbContext en el caso de código generado por VS2013), en concreto redefinir el método OnModelCreating:

  1. protected override void OnModelCreating(DbModelBuilder modelBuilder)
  2. {
  3.     base.OnModelCreating(modelBuilder);
  4.     modelBuilder.Entity<IdentityUser>().ToTable("Users").
  5.         Property(u => u.PasswordHash).HasColumnName("Pwd");
  6.     modelBuilder.Entity<ApplicationUser>().ToTable("Users").
  7.         Property(u => u.PasswordHash).HasColumnName("Pwd");
  8. }

Y ahora el esquema que tengo en la BBDD es (mantengo mi clase ApplicationUser con la propiedad BirthDay):

image

De nuevo… comparado con lo que se tenía que hacer con el antiguo Membership es una maravilla.

El tipo de cambios que puedes hacer vienen limitados por EF Code First, pero en general estamos ante un modelo mucho más flexible que el anterior Membership.

En resumen, a pesar de que ASP.NET Identity tiene varias cosillas que no me gustan (o mejor dicho, realmente varias cosas que echo en falta) es sin duda un paso adelante respecto al anterior Membership. Me sigue quedando la duda de si lo que ofrece es suficiente para según que tipo de webs, pero si las funcionalidades que ofrece te son suficientes y no te importa ajustar tu esquema a algunos detalles menores (como esos Id de usuario de tipo string) es un avance muy significativo respecto a Membership.

Un saludo a todos y… ¡felices fiestas!

Integrando oAuth con NancyFx (ii) – Katana

En el post anterior vimos como autenticar una aplicación NancyFx usando oAuth a través del paquete WorldDomination (o SimpleAuthentication que es el aburrido nombre que tiene ahora :p).

Pero dado que NancyFx puede funcionar como un componente OWIN y la estructura modular de OWIN permite que haya módulos de autenticación que se ejecuten antes en el pipeline, porque no “eliminar” toda responsabilidad sobre autenticación de NancyFx? Y que sea algún módulo OWIN el que lo haga no? A fin de cuentas, esa es la gracia de OWIN. En este post vamos a ver como integrar los módulos OWIN de autenticación que tiene Katana con NancyFx. Repasa el post anterior y haz lo siguiente:

  1. Crea una aplicación web vacía y añade los paquetes de Nancy, Nancy.Owin y Microsoft.Host.SystemWeb.
  2. Crea la clase de inicialización OWIN para que use Nancy.
  3. Crea un  módulo que redirija las peticiones a la URL / a una vista que muestre un enlace de login with twitter (que vaya p. ej. a /login/twitter).
  4. Crea otro módulo que redirija las peticiones de /secured a otra vista (basta que muestre un contenido tipo “Esto es seguro”.

Quédate aquí. En este punto puedes tanto acceder a / como a /secured obviamente, y pulsar en enlace “login with twitter” te generará un 404.

Pero ahora estamos listos para empezar.

Nota: Para este post vamos a usar la misma aplicación en twitter (que tenía el callback a /authentication/authenticatecallback. Aunque ahora la URL de callback puede ser la que queramos.

Dejando que Katana hable con Twitter…

Katana incorpora varios componentes de autenticación y hay uno que se encarga precisamente de gestionar el flujo oAuth con twitter. Este componente se llama Microsoft.Owin.Security.Twitter así que añádelo al proyecto. Para entendernos es el equivalente al WorldDomination pero en un mundo OWIN.

Una vez hayas añadido este paquete el primer paso es modificar la clase de inicio de OWIN para añadir el módulo de autenticación por Twitter:

  1. app.UseTwitterAuthentication(new TwitterAuthenticationOptions()
  2. {
  3.     ConsumerKey = "TU COMSUMER KEY",
  4.     ConsumerSecret = "TU CONSUMER SECRET"
  5. });

Por supuesto pon el consumer key y consumer secret de tu aplicacion.

En este punto si pulsas el enlace de “login with twitter” recibirás… un 404 de Nancy. Pues este enlace apunta a /login/twitter (o a la URL que tu hayas elegido, en el fondo da igual) y es una URL que no está enrutada. A diferencia del post anterior donde WorldDomination ya gestionaba la URL “/authentication/redirect/twitter” el módulo de Katana no gestiona ninguna URL. En su lugar “entra en acción” tan buen punto se recibe un 401.

Así que nada, vamos a añadir un  módulo que enrute la URL /login/twitter y devuelva un 401:

  1. public class AuthTwitterModule : NancyModule
  2. {
  3.     public AuthTwitterModule()
  4.     {
  5.         Get["/login/twitter"] = _ => new Response()
  6.         {
  7.             StatusCode = HttpStatusCode.Unauthorized
  8.         };
  9.     }
  10. }

Ahora si navegas a /login/twitter lo que recibirás es… bueno un 401 😛 La razón es porque aunque el módulo de Katana entra en acción cuando recibe un 401, no basta solo con el 401. Antes requiere que se rellene el entorno de OWIN con cierta información.

El entorno de OWIN es un diccionario de objetos, literalmente un IDictionary<string, object> que contiene toda la información del pipeline de OWIN. En OWIN no hay objetos tipo Request, Response o HttpContext porque eso implicaría que existe alguna DLL principal de OWIN y OWIN no pretende eso: se basa en tipos de .NET (Hay una sola excepción a este caso y es la interfaz IAppBuilder que está definida en el paquete Owin). Así los módulos OWIN se pasan información entre ellos a través de ese diccionario compartido.

Por lo tanto antes de devolver el 401 debemos meter cierta información en el entorno de OWIN para que el módulo de autenticación de Katana sepa que queremos autenticarnos via Twitter. ¿Qué método hay en NancyFx para meter código antes del código que procesa la petición? Exacto, el module hook Before. Pero en este caso no añadiremos el código directamente en el Before (podríamos) pero lo haremos más reutilizable a través de métodos de extensión (así seria aplicable a más de un módulo).

Pero primero necesitamos un método de extensión que me permita obtener el entorno de OWIN. El paquete Nancy.Owin (que es quien gestiona la integración de NancyFx en OWIN) deja el entorno OWIN dentro de la clave NancyOwinHost.RequestEnvironmentKey del contexto de NancyFx:

  1. public static class NancyContextExtensions
  2. {
  3.     public static OwinContext GetOwinContext(this NancyContext context)
  4.     {
  5.         var environment = (IDictionary<string, object>)context.Items[NancyOwinHost.RequestEnvironmentKey];
  6.         var owinContext = new OwinContext(environment);
  7.         return owinContext;
  8.     }
  9. }

A partir de la información del entorno se crea una variable de tipo OwinContext. La clase OwinContext no es estandard OWIN. Esta clase es una clase de Katana. Así que para que no queden dudas: la integración que estamos haciendo es entre NancyFx y los componentes de Katana. De hecho no es posible una integración universal porque la especificación de OWIN no define el nombre de las claves del entorno que los módulos deben usar, salvo unas cuantas (que podéis encontrar en la especificación de OWIN). Así pues la clase OwinContext no es nada más que el entorno de OWIN, pero visto a través de algo más tipado que un IDictionary<string, object> y que además entiende las claves que usan los módulos de Katana.

Vale, ahora que ya tenemos como obtener el entorno OWIN, vamos a añadir otro método de extensión, pero ahora contra la clase NancyModule:

  1. static class NancyModuleExtensions
  2. {
  3.     public static void Challenge(this NancyModule module, string redirectUri, string userId)
  4.     {
  5.         module.AddBeforeHookOrExecute(ctx =>
  6.         {
  7.             var properties = new AuthenticationProperties() { RedirectUri = redirectUri };
  8.             if (userId != null)
  9.             {
  10.                 properties.Dictionary["XsrfId"] = userId;
  11.             }
  12.             module.Context.GetOwinContext().Authentication.Challenge(properties, "Twitter");
  13.             return null;
  14.         }, "Challenge");
  15.     }
  16. }

Lo que estamos haciendo es añadir código al module hook Before del módulo de NancyFx al que se llame este método (sería lo más parecido a crear un filtro en ASP.NET MVC que hay en Nancy). Básicamente lo que hacemos en el Before es llamar al método Challenge que proporciona Katana que es el que se encarga de todo lo necesario.

Ahora tenemos que modificar el AuthTwitterModule para añadir la llamada a este método de extensión:

  1. public AuthTwitterModule()
  2. {
  3.     this.Challenge("/auth/redirect", null);
  4.     Get["/login/twitter"] = _ => new Response()
  5.     {
  6.         StatusCode = HttpStatusCode.Unauthorized
  7.     };
  8. }

Ahora sí, si navegas a /login/twitter empezará el flujo de oAuth y después serás redirigido a la URL que hemos usado como primer parámetro de la llamada a Challenge, es decir /auth/redirect con independencia del valor de callback que teníamos especificado en la aplicación de twitter.

En esta URL de callback (/auth/redirect) tenemos que recoger los valores que nos haya devuelto el proveedor de oAuth. Para ello nos vamos a apoyar en otro componente de Katana, el paquete Microsoft.AspNet.Identity.Owin, así que añade este paquete ahora. Una vez lo hayas añadido podemos usar el método GetExternalLoginInfoAsync.

Este método es asíncrono, así que lo invocaremos con await. Para poder usar await en NancyFx tenemos que declarar que la ruta que responde a /auth/redirect es asíncrona:

  1. Get["/auth/redirect", true] = async (_, ct) =>
  2. {
  3.     var authManager = Context.GetOwinContext().Authentication;
  4.     var loginInfo = await authManager.GetExternalLoginInfoAsync();
  5.     // …
  6. };

Este método se encarga de obtener los datos que nos envía el proveedor de oAuth. En este punto podemos recoger los datos del usuario y crear un ClaimsIdentity (esa clase es la nueva clase base de todas las identity en .NET). Lo más normal sería delegar en el Identity Membership para esto, pero, para no saturar, hagámoslo a mano. En el siguiente post veremos como integrarnos con  el Identity Membership. El código podría ser algo como así:

  1. Get["/auth/redirect", true] = async (_, ct) =>
  2. {
  3.     var authManager = Context.GetOwinContext().Authentication;
  4.     var loginInfo = await authManager.GetExternalLoginInfoAsync();
  5.     if (loginInfo != null)
  6.     {
  7.         authManager.SignOut(DefaultAuthenticationTypes.ExternalCookie);
  8.  
  9.         var identity = new ClaimsIdentity(DefaultAuthenticationTypes.ApplicationCookie.ToString(),
  10.             "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
  11.             "http://schemas.microsoft.com/ws/2008/06/identity/claims/role");
  12.         identity.AddClaim(new Claim("Player", "True"));
  13.         identity.AddClaim(new Claim("http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier",
  14.             loginInfo.DefaultUserName,
  15.             "http://www.w3.org/2001/XMLSchema#string"));
  16.         authManager.SignIn(new AuthenticationProperties() { IsPersistent = false }, identity);
  17.     }
  18.  
  19.     return new RedirectResponse("/secured");
  20. };

No te preocupes si no entiendes exactamente el código (como digo eso suele delegarse en el Identity Membership), pero básicamente creamos el ClaimsIdentity y le añadimos un nombre de usuario, así como una claim personalizada (Player con valor True).

Al final redirigimos al usuario a la URL /secured, una URL que se supone solo debe poder verse si el usuario no está autenticado.

El código del módulo Nancy que contiene la ruta para dicha URL es el siguiente:

  1. public class SecuredModule : NancyModule
  2. {
  3.     public SecuredModule()
  4.     {
  5.         this.RequiresOwinAuth();
  6.         Get["/secured"] = _ => View["secured.html"];
  7.     }
  8. }

El método RequiresOwinAuth es un método de extensión que nos hemos creado. Dicho método comprueba que existe una ClaimsIdentity en el contexto OWIN:

  1. public static void RequiresOwinAuth(this NancyModule module)
  2. {
  3.     module.AddBeforeHookOrExecute(ctx =>
  4.     {
  5.         var user = ctx.GetOwinUser();
  6.         return user == null || !user.Identity.IsAuthenticated ?
  7.             new Response() {StatusCode = HttpStatusCode.Unauthorized} :
  8.             null;
  9.     }, "OwinUser Not Found");
  10. }

(Este método de extensión sería el equivalente a aplicar [Authorize] en un controlador ASP.NET MVC).

Vale… ya casi lo tenemos, ahora tan solo nos falta configurar el pipeline OWIN para añadir la seguridad por cookies:

  1. app.UseCookieAuthentication(new CookieAuthenticationOptions()
  2. {
  3.     AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie
  4. });

Añadimos este código al principio del método de inicialización de OWIN, para que este módulo esté en el principio del pipeline.

Y ¡voilá! hemos terminado. Si nada más iniciar la aplicación navegas a /secured recibirás un 401 (Unauthorized). Si entras en twitter, después de hacer el login verás como se te redirige a /secured y ahora si ves el contenido seguro.

Por supuesto, puedes hacer que en lugar de ver un 401 el usuario sea redirigido a una página de Login, simplemente cambiando la configuración del proveedor de seguridad por cookies:

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

Ahora si nada más empezar navegas a /secured verás que el usuario es redirigido a /login (en este caso verás un 404 ya que no hay ninguna ruta que responda a la URL /login).

Bueno… en el post anterior vimos como configurar NancyFx junto con WorldDomination para soportar login por oAuth. En este post hemos ido un paso más allá sustituyendo toda la autenticación por componentes OWIN, en lugar de que toda la responsabilidad esté gestionada por NancyFx.

Un saludo!

PD: Tenéis el código en mi carpeta de SkyDrive (fichero NancyKatanaIIS2).