Tecnología de vanguardia

Autenticación externa con ASP.NET Identity

Dino Esposito

Dino EspositoVisual Studio 2013 incluye nuevas plantillas de proyecto de ASP.NET MVC 5 y Web Forms para compilar aplicaciones. De forma opcional, las plantillas de ejemplo pueden contener una capa de autenticación predeterminada basada en ASP.NET Identity. El código generado contiene muchas características, pero puede que no sea tan sencillo de leer. En este artículo, mostraré cómo realizar la autenticación a través de un servidor externo basado en OAuth u OpenID, por ejemplo, una red social, y los pasos que quizás quieras seguir una vez que se complete la autenticación y tengas acceso a todas las notificaciones descargadas.

Comenzaré con una aplicación ASP.NET MVC 5 vacía y agregaré la capa de autenticación mínima indispensable que puede necesitar una aplicación. Verás que el nuevo marco de ASP.NET Identity trae consigo un nuevo nivel de complejidad y potencia a toda la pila de autenticación; pero, a la vez, acelera y facilita la organización de la autenticación de usuarios externos sin que tengas que aprender nuevos paquetes ni API.

Exclusión del controlador de cuentas

Desde la primera versión de ASP.NET MVC, la aplicación de ejemplo, que da la primera impresión del marco a los nuevos usuarios, ofrecía una clase de controlador bastante voluminosa para administrar todos los aspectos de la autenticación. En mi columna de marzo de 2014, “Un primer vistazo a ASP.NET Identity” (msdn.microsoft.com/magazine/dn605872), expuse la información general sobre el marco de ASP.NET Identity a partir del código que obtienes en Visual Studio de forma predeterminada. Esta vez, en cambio, uniré las piezas a partir de un proyecto vacío de ASP.NET MVC 5. Al hacerlo, seguiré con cuidado el principio de responsabilidad única (bit.ly/1gGjFtx).

Comenzaré por un proyecto vacío y agregaré el scaffolding de ASP.NET MVC 5. A continuación, agregaré dos nuevas clases del controlador: Login­Controller y AccountController. La primera se relaciona únicamente con las operaciones de inicio y cierre de sesión, ya sea a través del sistema de suscripción local específico del sitio o un proveedor de OAuth externo, como Twitter. Como el nombre lo indica, la clase AccountController incluye las demás funcionalidades, relacionadas principalmente con la administración de las cuentas de usuario. Por simplicidad, solo incluiré la capacidad de registrar un nuevo usuario en este momento. La figura 1 muestra el esquema de las dos clases del controlador, según se ven en Visual Studio 2013.

Skeleton of LoginController and AccountController Classes in Visual Studio 2013

Figura 1 Esquema de las clases LoginController y AccountController en Visual Studio 2013

La interfaz de usuario se articula en tres vistas parciales: usuario conectado, formulario de inicio de sesión local y formulario de inicio de sesión social, como se ve en la figura 2.

The Three Partial Views Used in the Authentication Stack

Figura 2 Las tres vistas parciales usadas en la pila de autenticación

La clase IdentityController

En el esquema que se muestra en la figura 1, tanto la clase LoginController como la clase AccountController heredan de la clase base del controlador de ASP.NET MVC 5. No obstante, si vas a usar el marco de ASP.NET Identity, puede que esta no sea la opción ideal, ya que puede provocar una duplicación del código. Sugiero crear un proyecto temporal en Visual Studio 2013 y echar un vistazo al código predeterminado almacenado en la clase omnipotente y abarcadora AccountController. ASP.NET Identity requiere que insertes la clase UserManager<TUser> y el mecanismo de almacenamiento en el controlador. Además, puede que quieras tener un par de métodos de ayuda para simplificar el proceso de inicio de sesión y redireccionamiento de los usuarios. También se necesita un par de propiedades para referir el objeto raíz UserManager y tal vez incluso otra propiedad de ayuda para establecer un punto de contacto con el middleware Open Web Interface para .NET (OWIN).

Probablemente, sea conveniente introducir una clase intermedia donde todo este código reutilizable se pueda almacenar de forma cómoda una vez y solo una vez. Llamaré a esta clase IdentityController y la definiré como se muestra en la figura 3.

Figura 3 Una nueva clase básica para la autenticación de ASP.NET Identity

using System.Collections.Generic; using System.Security.Claims; using System.Threading.Tasks; using System.Web; using System.Web.Mvc; using AuthSocial.ViewModels.Account.Identity; using Microsoft.AspNet.Identity; using Microsoft.AspNet.Identity.EntityFramework; using Microsoft.Owin.Security; namespace AuthSocial.Controllers { public class IdentityController<TUser, TDbContext> : Controller where TUser : IdentityUser where TDbContext : IdentityDbContext, new() { public IdentityController() : this(new UserManager<TUser>( new UserStore<TUser>(new TDbContext()))) { } public IdentityController(UserManager<TUser> userManager) { UserManager = userManager; } protected UserManager<TUser> UserManager { get; set; } protected IAuthenticationManager AuthenticationManager { get { return HttpContext.GetOwinContext().Authentication; } } protected async Task SignInAsync( TUser user, bool isPersistent) { AuthenticationManager.SignOut( defaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, identity); } protected ActionResult RedirectToLocal(string returnUrl) { if (Url.IsLocalUrl(returnUrl)) { return Redirect(returnUrl); } return RedirectToAction("Index", "Home"); } protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim(ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); } } }

El uso de genéricos (y las limitaciones relacionadas) obliga a la clase IdentityController a usar cualquier definición de un usuario basada en la clase IdentityUser y cualquier definición de un contexto de almacenamiento basado en IdentityDbContext.

En particular, IdentityUser es la clase básica para describir un usuario y ofrece un conjunto mínimo de propiedades. Tienes toda la libertad para crear clases nuevas y más específicas con la herencia sencilla. Al hacerlo, especifica la clase personalizada en la declaración de clases derivada de IdentityController. He aquí una declaración de LoginController y AccountController basada en IdentityController<TUser, TDbContext>:

public class LoginController : IdentityController<IdentityUser, IdentityDbContext> { ... } public class AccountController : IdentityController<IdentityUser, IdentityDbContext> { ... }

De este modo, sientas las bases para particionar la mayor parte de la complejidad del código de autenticación y administración de usuarios en tres clases. Además, ahora tienes una clase para proporcionar servicios comunes y una clase que se centra solo en las tareas de inicio de sesión. Finalmente, tienes una clase independiente para tratar la administración de las cuentas de usuario. Voy a empezar con la clase de cuentas, donde para simplificar solo expondré un extremo para registrar nuevos usuarios.

La clase AccountController

En general, se necesita un método de controlador solo si hay algún elemento de la interfaz de usuario que se muestra con el que interactúe el usuario final. Si, por ejemplo, hay un botón de envío en el que el usuario pueda hacer clic, necesitas un método de controlador sensible a POST. Para definir este método, comienza a partir del modelo de vista. La clase del modelo de vista es un objeto de transferencia de datos (DTO) que recopila los datos (cadenas, fechas, números, booleanos y colecciones) que entran y salen de la interfaz de usuario que se muestra. Para un método que agregue un nuevo usuario en el sistema de suscripción local, puedes tener la siguiente clase del método de vista (el código fuente es el mismo que el que se obtiene de la plantilla de ASP.NET MVC 5 para Visual Studio 2013):

public class RegisterViewModel { public string UserName { get; set; } public string Password { get; set; } public string ConfirmPassword { get; set; } }

Es posible que también quieras representar esta clase con anotaciones de datos para fines de validación y visualización. Sin embargo, en el nivel de diseño superior, lo único que importa es que la clase cuente con una propiedad para cada control de entrada en la interfaz de usuario real. La figura 4 presenta la implementación del método de controlador que administra la solicitud POST para registrar a un nuevo usuario.

Figura 4 Registro de un nuevo usuario

public async Task<ActionResult> Register( RegisterViewModel model) { if (ModelState.IsValid) { var user = new IdentityUser { UserName = model.UserName }; var result = await UserManager.CreateAsync( user, model.Password); if (result.Succeeded) { await SignInAsync(user, false); return RedirectToAction("Index", "Home"); } Helpers.AddErrors(ModelState, result); } return View(model); }

El método CreateAsync en la clase UserManager tan solo usa el mecanismo de almacenamiento subyacente para crear una nueva entrada para el usuario especificado. Esta clase AccountController también es el lugar ideal para definir otros métodos para editar o simplemente eliminar cuentas de usuario y cambiar o restablecer contraseñas.

La clase LoginController

El controlador de inicio de sesión presentará métodos para que los usuarios inicien y cierren sesión en cualquiera de las formas aceptadas por la aplicación. Al igual que como lo mencioné para el controlador de cuentas, el método SignIn administrará un DTO con propiedades para transportar credenciales, un indicador para la persistencia de autenticación y, tal vez, una URL de retorno. En el código predeterminado, la URL de retorno se administra con ViewBag; también se puede incorporar a la clase de modelo de vista:

public class LoginViewModel { public string UserName { get; set; } public string Password { get; set; } public bool RememberMe { get; set; } public string ReturnUrl { get; set; } }

El código para interactuar con el sistema de suscripción local, totalmente administrado en el servidor de aplicaciones, se pega desde el proyecto predeterminado. El siguiente es un extracto de la implementación de POST del nuevo método SignIn:

var user = await UserManager.FindAsync( model.UserName, model.Password); if (user != null) { await SignInAsync(user, model.RememberMe); return RedirectToLocal(returnUrl); }

El código que en definitiva permite a los usuarios iniciar sesión es un método (llamado SignInAsync) que se toma prestado de la implementación predeterminada y se reescribe como método protegido en la clase básica IdentityController:

protected async Task SignInAsync(TUser user, bool isPersistent) { AuthenticationManager.SignOut( DefaultAuthenticationTypes.ExternalCookie); var identity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = isPersistent }, identity); }

Al usar el marco de ASP.NET Identity, no usas ninguna clase que se relacione explícitamente con la maquinaria de ASP.NET. En el contexto de un host de ASP.NET MVC, todavía administras la autenticación de usuarios con formularios de inicio de sesión y cookies de autenticación de ASP.NET, salvo que no usas la clase FormsAuthentication de ASP.NET directamente. Hay dos capas de código con las que deben lidiar los desarrolladores clásicos de ASP.NET. Una es la API simple de ASP.NET Identity representada por clases, como UserManager y IdentityUser. La otra es el middleware de autenticación por cookies de OWIN, que esconde los pequeños detalles de cómo se almacena y conserva la información de un usuario conectado a la API de autenticación de fachada. Todavía se usa la autenticación mediante formularios y todavía se crea la vieja y conocida cookie .ASPXAUTH. Pero todo sucede tras las bambalinas de un montón de nuevos métodos. Para comprenderlo mejor, visita la publicación “A Primer on OWIN Cookie Authentication Middleware for the ASP.NET Developer” (Un acercamiento al middleware de autenticación por cookies de OWIN para el desarrollador de ASP.NET) en el blog de Brock Allen en bit.ly/1fKG0G9.

Observa el código siguiente que se encuentra invocado directamente desde Startup­Auth.cs, el punto de entrada del middleware de OWIN:

app.UseCookieAuthentication( new CookieAuthenticationOptions { AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Login/SignIn") }); app.UseExternalSignInCookie( DefaultAuthenticationTypes.ExternalCookie); app.UseTwitterAuthentication( consumerKey: "45c6...iQ", consumerSecret: "pRcoXT...kdnU");

Este código es una especie de interfaz fluida que usas en lugar de las antiguas entradas de autenticación web.config. Además, también vincula la autenticación basada en Twitter y configura el middleware de OWIN para usar ExternalSignInCookie para almacenar temporalmente información sobre el inicio de sesión de un usuario a través de un proveedor de inicio de sesión de terceros.

El objeto AuthenticationManager representa el punto de entrada en el middleware de OWIN y la fachada por la cual se invoca a la API de autenticación de ASP.NET subyacente basada en FormsAuthentication.

El efecto neto es que se termina usando una API unificada para autenticar usuarios y comprobar credenciales, tanto si el sistema de suscripción es local en el servidor web como si se delega externamente a un proveedor de inicio de sesión de una red social. Este es el código que usarías si se acepta que los usuarios del sitio se autentiquen a través de sus cuentas de Twitter:

[AllowAnonymous] public ActionResult Twitter(String returnUrl) { var provider = "Twitter"; return new ChallengeResult(provider, Url.Action("ExternalLoginCallback", "Login", new { ReturnUrl = returnUrl })); }

Lo primero y más importante es que tienes que tener una aplicación de Twitter configurada y asociada a una cuenta de desarrollador de Twitter. Para obtener más información, consulta dev.twitter.com. Una aplicación de Twitter se identifica exclusivamente con un par de cadenas alfanuméricas conocidas como clave y secreto de consumidor.

El código predeterminado que se obtiene del asistente del proyecto de ASP.NET MVC 5 también sugiere que uses una clase totalmente nueva (ChallengeResult) para administrar los inicios de sesión externos, como se ve en la figura 5.

Figura 5 La clase ChallengeResult

public class ChallengeResult : HttpUnauthorizedResult { public ChallengeResult(string provider, string redirectUri) { LoginProvider = provider; RedirectUri = redirectUri; } public string LoginProvider { get; set; } public string RedirectUri { get; set; } public override void ExecuteResult(ControllerContext context) { var properties = new AuthenticationProperties { RedirectUri = RedirectUri }; context.HttpContext .GetOwinContext() .Authentication .Challenge(properties, LoginProvider); } }

La clase delega al middleware de OWIN, en el método llamado Challenge, la tarea de conectarse al proveedor de inicio de sesión especificado (registrado al inicio) para la autenticación. La URL de retorno es el método ExternalLoginCallback en el controlador de inicio de sesión:

[AllowAnonymous] public async Task<ActionResult> ExternalLoginCallback( string returnUrl) { var info = await AuthenticationManager.GetExternalLoginInfoAsync(); if (info == null) return RedirectToAction("SignIn"); var identity = GetBasicUserIdentity(info.DefaultUserName); AuthenticationManager.SignIn( new AuthenticationProperties { IsPersistent = true }, identity); return RedirectToLocal(returnUrl); }

El método GetExternalLoginInfoAsync recupera información sobre el usuario identificado que el proveedor de inicio de sesión pone a disposición. Cuando vuelve el método, la aplicación tiene información suficiente para que el usuario pueda iniciar sesión, pero no pasa nada todavía. Es importante conocer las opciones que hay disponibles. En primer lugar, se puede proceder a crear una cookie de autenticación canónica de ASP.NET, como en mi ejemplo. El método de ayuda GetBasicUserIdentity, definido en la clase básica IdentityController, crea un objeto ClaimsIdentity relacionado con el nombre de la cuenta de Twitter proporcionado:

protected ClaimsIdentity GetBasicUserIdentity(string name) { var claims = new List<Claim> { new Claim( ClaimTypes.Name, name) }; return new ClaimsIdentity( claims, DefaultAuthenticationTypes.ApplicationCookie); }

Una vez que el usuario de Twitter autoriza a la aplicación de Twitter a compartir información, la página web recibe el nombre de usuario y posiblemente más datos, como se ve en la figura 6 (esto depende de la API social que realmente se use).

The Classic Twitter-Based Authentication
Figura 6 La autenticación clásica basada en Twitter

Observa en el código anterior que en el método ExternalLoginCallback no se agrega ningún usuario al sistema de suscripción local. Este es el escenario más sencillo. En otras situaciones, puede que quieras usar la información de Twitter para registrar mediante programación un vínculo entre un inicio de sesión local y el nombre de usuario autenticado externamente (esta es la solución que se presenta en el código de autenticación predeterminado del asistente). Finalmente, puedes decidir redirigir los usuarios a una página diferente para que puedan introducir una dirección de correo o simplemente registrarse en el sitio.

Mezcla de autenticación clásica y social

Es necesario cierto nivel de autenticación en casi todos los sitios web. ASP.NET Identity facilita codificar y mezclar la autenticación clásica y social detrás de una fachada unificada. Aun así, es importante tener una estrategia clara sobre lo que se quiere para administrar las cuentas de usuarios y qué información se quiere recopilar y almacenar sobre ellos. Mezclar la autenticación clásica y social hace que tengas una cuenta de usuario individual y la asocies con varios proveedores de inicio de sesión, como Facebook, Twitter y los demás proveedores de inicio de sesión compatibles con ASP.NET Identity. De este modo, los usuarios tienen varias formas de iniciar sesión, pero siguen siendo uno a los ojos de tu aplicación. Además, se pueden agregar o eliminar proveedores de inicio de sesión según se quiera.

Dino Esposito es el autor de “Architecting Mobile Solutions for the Enterprise” (Microsoft Press, 2012) y del próximo libro “Programming ASP.NET MVC 5” (Microsoft Press). Como evangelizador técnico para las plataformas .NET Framework y Android en JetBrains y orador frecuente en eventos mundiales de la industria, Esposito comparte su visión sobre el software en software2cents.wordpress.com y en Twitter en twitter.com/despos.

Gracias al siguiente experto técnico de Microsoft por su ayuda en la revisión de este artículo: Pranav Rastogi