Introducción a Identityjsdelacruz.16mb.com/materialpad/finales/Final Identity... · Web viewEl...

40
Trabajo Final - ASP.NET Identity Facundo Javier Gelatti - Programación de Aplic. Distribuidas Introducción a Identity La tecnología de la familia .NET que elegí para el trabajo es ASP.NET Identity. Esta tecnología nos ofrece facilidades para la implementación de servicios de gestión de usuarios, autenticación y autorización dentro de nuestras aplicaciones. Estos servicios constituyen funciones imprescindibles para cualquier aplicación web actual. Si bien la plataforma ya ofrecía estos servicios antes (con ASP.NET Membership), Identity surgió como respuesta a los cambios que fueron ocurriendo en las formas de autenticación utilizadas en las aplicaciones web. Actualmente, no solamente hay que preocuparse más por la seguridad (por ejemplo, al usar técnicas de autenticación por dos factores), sino que el supuesto de que los usuarios se van a registrar localmente en nuestra aplicación simplemente con un nombre de usuario y contraseña para identificarse, ya no sigue siendo válido. Hoy en día, los usuarios interactúan con servicios web, y entre sí a través de redes sociales, como Facebook o Twitter, y ya tienen sus datos registrados en esos servicios. Podemos mejorar la experiencia de uso de nuestras aplicaciones si eliminamos la necesidad del usuario de registrar todos sus datos manualmente (de nuevo) en un lugar más, al poder interactuar con servicios que el usuario ya usa, utilizándolos como proveedores para las credenciales de autenticación y los datos de los usuarios. Objetivos de Identity Considerando la ubicuidad de las funciones de autenticación y autorización, además de los cambios que mencionamos, los objetivos de Identity son [1]: 1

Transcript of Introducción a Identityjsdelacruz.16mb.com/materialpad/finales/Final Identity... · Web viewEl...

Trabajo Final - ASP.NET IdentityFacundo Javier Gelatti - Programación de Aplic. Distribuidas

Introducción a IdentityLa tecnología de la familia .NET que elegí para el trabajo es ASP.NET Identity.Esta tecnología nos ofrece facilidades para la implementación de servicios de gestión de usuarios, autenticación y autorización dentro de nuestras aplicaciones.Estos servicios constituyen funciones imprescindibles para cualquier aplicación web actual. Si bien la plataforma ya ofrecía estos servicios antes (con ASP.NET Membership), Identity surgió como respuesta a los cambios que fueron ocurriendo en las formas de autenticación utilizadas en las aplicaciones web.Actualmente, no solamente hay que preocuparse más por la seguridad (por ejemplo, al usar técnicas de autenticación por dos factores), sino que el supuesto de que los usuarios se van a registrar localmente en nuestra aplicación simplemente con un nombre de usuario y contraseña para identificarse, ya no sigue siendo válido. Hoy en día, los usuarios interactúan con servicios web, y entre sí a través de redes sociales, como Facebook o Twitter, y ya tienen sus datos registrados en esos servicios. Podemos mejorar la experiencia de uso de nuestras aplicaciones si eliminamos la necesidad del usuario de registrar todos sus datos manualmente (de nuevo) en un lugar más, al poder interactuar con servicios que el usuario ya usa, utilizándolos como proveedores para las credenciales de autenticación y los datos de los usuarios.

Objetivos de IdentityConsiderando la ubicuidad de las funciones de autenticación y autorización, además de los cambios que mencionamos, los objetivos de Identity son [1]:

● Proveer un sistema unificado para la autenticación y la autorización. Identity puede ser utilizado en aplicaciones MVC y Web Forms.

● Facilitar la inclusión de los datos del usuario, en forma personalizada. Se pueden agregar, de manera sencilla, datos adicionales que sean necesarios para el perfil de los usuarios.

● Control de la persistencia. Identity se encarga de la persistencia de todos los datos que involucran a los usuarios.

● Proveer roles a los usuarios, para facilitar la organización al momento de autorizarlos para que realicen (o no) ciertas acciones.

● Soportar proveedores para el inicio de sesión social. Se pueden agregar inicios de sesión sociales, que utilicen cuentas de Facebook, Twitter, Google o Microsoft.

● Integración con OWIN. OWIN es un estándar abierto, que define una interfaz para la comunicación entre servicios web .NET y aplicaciones web.

● Distribución mediante un paquete NuGet, facilitando así la obtención y la actualización de Identity por parte de los desarrolladores.

El código fuente de Identity se encuentra disponible en github.com/aspnet/Identity

1

Conceptos PreviosAutorización y autenticaciónCuando hablamos de manera informal, muchos pensamos que los términos autorización y autenticación se refieren a lo mismo, cuando en realidad no es así.La autenticación es la comprobación de las credenciales del usuario. Durante este proceso, se identifica al usuario dentro de la aplicación. Dicho de otra forma, la autenticación responde a la pregunta ¿Quién está utilizando la aplicación?La autorización comprueba qué acciones puede realizar (o no) el usuario en la aplicación. Usualmente se realiza después del proceso de autenticación. La autorización responde a la pregunta ¿Qué acciones puede (o tiene permitido) hacer el usuario?

En este trabajo, veremos los servicios que nos ofrece Identity, en cuanto a la autenticación (mediante datos propios de la aplicación, o mediante un proveedor externo para el inicio de sesión) y autorización (diferenciando a los usuarios anónimos de los no registrados, y a su vez clasificando a los usuarios registrados mediante roles).

OAuthDurante la introducción, mencionamos que uno de los objetivos de Identity era facilitar la inclusión en nuestra aplicación de características de inicio de sesión social, lo que requiere que se soporten proveedores externos para el inicio de sesión. Para esto, Identity utiliza el estándar OAuth.

OAuth es un estándar abierto para la autorización de usuarios [3], que proporciona un acceso seguro a los recursos almacenados en un servicio web, por parte de aplicaciones, a nombre de usuarios cuyas credenciales se encuentran registradas en el servicio. Esto permite que nuestras aplicaciones puedan:

1. Aceptar el registro de los usuarios utilizando un servicio existente, sin que sea necesario compartir las credenciales de dicho servicio con las aplicaciones.

2. Solicitar recursos a nombre del usuario, con un acceso controlado.

Este trabajo va a mostrar como realizar la función (1), utilizando Identity. Una vez obtenido el acceso al servicio, adicionalmente puede realizarse la función (2), esto es, solicitar recursos extra (tales como la foto de perfil del usuario, o su lista de amigos), pero en este trabajo no profundizaremos sobre ese punto.

2

FuncionamientoEsencialmente, OAuth funciona de la siguiente manera: las aplicaciones clientes realizan las solicitudes al servidor de autorización (el proveedor). El usuario se autentica con el servicio, y se emite un token de acceso para ser usado por la aplicación cliente, que utiliza ese token para poder acceder a los recursos del servicio (por supuesto, con la debida autorización del usuario).

La siguiente figura ilustra el proceso que se sigue, mediante un diagrama de secuencia:

Nótese que el usuario inicia sesión en forma directa con el servicio, por lo que las credenciales del usuario para el acceso al servicio nunca son compartidas con nuestra aplicación, solo el token de acceso.

Conceptos de IdentityUsuarios autenticados localmente y en forma remotaLos usuarios autenticados localmente son los que se han autenticado sin la necesidad de una comunicación con un tercero. El código para la autenticación existe en el mismo servidor que la aplicación, y usualmente está basado en datos almacenados en una base de datos local.

Los usuarios autenticados en forma remota son los que se autentican mediante un servicio externo, que juega el rol de proveedor de inicio de sesión (esto lo discutimos en más detalle en la sección de OAuth). Aunque estos usuarios no utilizan los registros locales para su autenticación, esto no quiere decir que nuestra aplicación no mantendrá datos referidos a ellos. En la base de datos local se almacenará el resto de la información vinculada al usuario, que es necesaria luego de realizar el proceso de autenticación.

3

Modelo de DatosEs momento oportuno para discutir el conjunto de datos que utiliza Identity, a nivel persistencia1. Las entidades de datos utilizadas pueden resumirse de manera conceptual en el siguiente diagrama de entidad-relación:

Vemos que se almacenan los Usuarios locales (con su nombre y contraseña, entre otros datos), y los Roles de los mismos. Las entidades Logins y Claims merecen una explicación más detallada.Los Logins vinculan a los usuarios de la aplicación con los inicios de sesión que se realizaron mediante un proveedor externo (utilizando OAuth, por ejemplo). Se identifica al proveedor (que se corresponde con el servicio utilizado para iniciar sesión), y se registra también un identificador que utiliza ese servicio para identificar al usuario. Todo esto se vincula con Usuarios, esto es, los usuarios de nuestra aplicación.Los Claims representan datos que se asocian al usuario, mediante una estructura de clave-valor. En este trabajo no utilizaremos los Claims, pero los mencionamos brevemente aquí.

Si bien Identity no nos compromete a una implementación particular del modelo de datos resumido de la figura anterior, en este trabajo utilizaremos los recursos que nos ofrece Identity para su uso con Entity Framework, los cuales sí definen una implementación específica a nivel base de datos. La estructura de la base de datos utilizada por Identity con Entity Framework se ilustra en el siguiente modelo relacional:

1 Como Identity es una tecnología orientada a objetos, uno podría cuestionarse si es conveniente empezar su descripción por el modelo de datos, en vez de centrarse en los objetos. Hay que aclarar que en este trabajo vamos a concentrarnos en los recursos que nos ofrece Identity para su uso junto con Entity Framework (que se encarga del mapeo objetos-relacional). La realidad es que, después de haber estudiado los objetos que nos ofrece Identity para esto, la mayoría de las colaboraciones se basan en objetos que juegan el papel de “servicios”, que consumen y manipulan a otros que son, básicamente, estructuras de datos que reflejan de forma casi exacta al esquema de la base de datos. Es por esto que decidí incluir primero el modelo de datos.

4

Se omitieron algunas columnas de AspNetUsers, que no eran relevantes para los temas tratados en este trabajo, para no sobrecargar el diagrama.

Nótese que en ningún momento se almacenan los tokens obtenidos mediante OAuth. La tabla AspNetUserLogins registra al proveedor de inicio de sesión, en caso de haber iniciado sesión utilizando un servicio externo.

ArquitecturaLa arquitectura de Identity se basa en el patrón de diseño arquitectónico capas2. La estructura básica se muestra en la siguiente figura [2]:

2 Para más información sobre el estilo arquitectónico Capas, puede consultarse: https://msdn.microsoft.com/en-us/library/ee658117.aspx#LayeredStyle

5

Los componentes más importantes de la arquitectura son los Managers y las Stores.● Los Managers son objetos de alto nivel. Tienen responsabilidades como las de

crear un usuario, agregar una identidad, iniciar la sesión o validar los datos de autenticación. Se comunican con las Stores a través de interfaces.

● Las Stores se comunican con la capa de acceso a datos. Tienen la responsabilidad de guardar los usuarios, roles, etc.

● En la capa de acceso a datos se encuentran los componentes propios de Entity Framework, tales como el contexto de la base de datos.

Introducción de Implementaciones personalizadasLa arquitectura es flexible, en cuanto a que nos permite proveer nuestras propias implementaciones para cada uno de los componentes. Por ejemplo, si queremos personalizar el acceso y origen de datos (en caso de que no se quiera utilizar Entity Framework), podemos incluir nuestras propias implementaciones de las Stores, que se van a ser las que se comuniquen con nuestras capas de acceso y origen de datos.

6

Vamos a ilustrar el cambio suponiendo que queremos reemplazar la implementación de la UserStore. La interacción en tiempo de ejecución es la siguiente:

Solamente tenemos que pasarle una referencia de nuestra UserStore al UserManager. Esto puede hacerse de manera sencilla, solo hay que implementar las interfaces requeridas por la capa superior (en este caso, las que necesita el UserManager)3:

Hay muchas implementaciones ya existentes (por ejemplo para MySQL, Azure Table Storage, RavenDB, etc). Nosotros utilizaremos, como ya se dijo, las implementaciones proporcionadas por Identity para ser usadas con Entity Framework.

Resumen de las clases principalesEl siguiente diagrama representa un resumen de las clases principales dentro de Identity, en particular las que se utilizan con Entity Framework (implementación de Modelos y Stores), y cómo se relacionan entre sí. Se omitieron las interfaces, por lo que muchas de las dependencias que figuran en el diagrama son sólo en tiempo de ejecución.

3 Esto es posible gracias al polimorfismo, un concepto del paradigma de objetos que nos proporciona una gran ventaja y facilidad, a comparación de la solución alternativa utilizando el paradigma estructurado.

7

Owin y KatanaDurante la introducción, mencionamos que uno de los objetivos de Identity era proporcionar integración con OWIN. OWIN (Open Web Interface for .NET) es un estándar abierto, definida por los desarrolladores de ASP.NET, que tiene como finalidad desacoplar la aplicación web propiamente dicha del host sobre el que esta se ejecuta.La necesidad de hacer esto surgió a partir del hecho de que las aplicaciones desarrolladas con ASP.NET dependían fuertemente de System.Web (un componente particular del framework de .NET). La consecuencia de esto era que la única manera de alojar una

8

aplicación web basada en estas tecnologías de Microsoft era a través de su servidor web, Internet Information Services (IIS) [4]. Katana es una (no la única) implementación de OWIN (que es la especificación), por parte de Microsoft.

OWIN introduce el concepto de middleware, que representa a los componentes que reciben las peticiones del servidor para procesar una respuesta. En este trabajo se introducirán los conceptos básicos de OWIN, necesarios para poder realizar las funciones de autenticación de Identity, sin ahondar demasiado en el tema. Para más información, puede consultarse [6].

Owin con IdentityIdentity provee una integración completa con OWIN, aunque el uso de dicha integración es opcional4 (podemos utilizar Identity sin tener en cuenta los conceptos de OWIN). Sin embargo, si decidimos utilizar OWIN, tenemos a nuestra disposición diversos recursos adicionales, incluyendo a los middleware de seguridad, proporcionados por distintos proveedores para la autenticación.

Los componentes que tenemos a nuestra disposición son [2]:

Middleware de seguridad Identity (propiamente dicho)

Microsoft.Owin.Security.Cookies Microsoft.AspNet.Identity

Microsoft.Owin.Security.Oauth Microsoft.AspNet.Identity.EntityFramework

Microsoft.Owin.Security.Facebook

Microsoft.Owin.Security.Google

Microsoft.Owin.Security.MicrosoftAccount

Microsoft.Owin.Security.Twitter

Microsoft.Owin.Security

Como veremos más adelante en el trabajo, vamos a aprovechar estos componentes middleware de seguridad para poder implementar las funciones de autenticación de Identity.

4 Hay un ejemplo del uso de Identity sin necesidad de configurar owin de forma explícita en https://blogs.msdn.microsoft.com/webdev/2013/10/20/building-a-simple-todo-application-with-asp-net-identity-and-associating-users-with-todoes/

9

Aplicación PrácticaPara este trabajo final, partí de la aplicación realizada en mi Trabajo Práctico 3 (el último trabajo práctico que se realizó durante el cursado de la materia). La aplicación no hacía uso de Identity, por lo que vamos a mostrar cómo incorporar esta tecnología en las funciones ya existentes, además de agregar la posibilidad de iniciar sesión mediante Facebook. Como primera medida, vamos a describir de qué se trata la aplicación.

Descripción de la aplicaciónEl objetivo de la aplicación es el registro y seguimiento de tareas. Las tareas tienen una descripción y un estado (pendiente, en curso o terminada). A su vez, cada tarea pertenece a una categoría, que se identifica con un color. Los usuarios son los dueños de sus tareas, y cada usuario solo puede ver e interactuar con las tareas que él registró.

Capturas de pantallaA continuación, se incluyen capturas de las secciones principales de la aplicación:

Registro e inicio de sesión Lista de tareas (luego de iniciar sesión)

10

Diagrama de casos de usoLas funcionalidades ofrecidas por el sistema se resumen en el siguiente diagrama de casos de uso:

Tecnologías utilizadasLas principales tecnologías que fueron usadas para realizar la aplicación son las siguientes:

● ASP.NET MVC● EntityFramework (con code-first migrations)● Materialize (materializecss.com)

Incorporación de la tecnología de IdentityEl desafío de este trabajo es incorporar la tecnología de Identity en esta aplicación ya existente, no solo actualizando las funciones ya implementadas, sino también agregando la posibilidad de iniciar sesión a través de Facebook.

Motivación para el uso de IdentityLa aplicación que se presentó no utilizaba Identity para sus funciones de autenticación y autorización. Sin embargo, las mismas sí estaban implementadas. Vamos a describir brevemente la implementación original, y a analizar sus inconvenientes.

11

AutenticaciónPara mantener los datos de los usuarios se utilizaba una base de datos local. Durante el registro del usuario, se validaban sus datos, y se guardaban en la misma. La contraseña se encriptaba utilizando MD5. En el momento de iniciar sesión, luego de validar los datos (haciendo una comparación con los registrados), se guardaba una referencia al usuario en un slot de Session (propiedad de los Controllers en ASP.NET MVC).

Session["usuario"] = autenticador.Autenticar(nombre, contraseña);

AutorizaciónPara realizar la autorización, se sobreescribía el método OnActionExecuting de los Controllers en donde se debía realizar la autorización, verificando ahí que Session contenga al usuario que había iniciado sesión.

protected override void OnActionExecuting(ActionExecutingContext ctx){ if (Session["usuario"] == null) { ctx.Result = RedirectToAction("IniciarSesion", "Usuarios"); } usuarioActual = (Usuario) Session["usuario"]; //...}

Análisis del método utilizadoPodemos señalar que la ventaja principal del método utilizado es que es relativamente simple, fácil de entender y de implementación rápida para cuando la base de código es chica. Sin embargo, esto tiene su costo. Las desventajas son las siguientes:

● No es una solución estándar conocida. El problema de la autenticación y autorización es un problema ubicuo, y para ello existen diversas soluciones conocidas. Si utilizamos una solución no estándar, entonces se incrementa la dificultad de entendimiento del sistema para los nuevos miembros del proyecto, aún si estos conocen las soluciones más utilizadas.

● No es una solución extensible. No es trivial el agregado de funcionalidades nuevas, tales como el inicio de sesión mediante Facebook. Además, y volviendo un poco al punto anterior, no existen componentes prediseñados que puedan acoplarse y ser reutilizados en nuestra solución (ya que no es una implementación conocida).

● Sólo es posible realizar la autorización en controllers completos y de manera manual. El código para la autorización se va a repetir en cada controller en el que tengamos acciones que autorizar (si bien esto puede solucionarse utilizando el patrón de diseño Layer Supertype5, pero ya implica un trabajo extra). Además, hay

5 martinfowler.com/eaaCatalog/layerSupertype.html

12

que añadir lógica adicional si queremos autorizar solo algunas acciones, o si necesitamos incorporar niveles de usuario.

● Pueden existir huecos de seguridad no considerados. Si bien esto ocurre siempre, pero estamos más seguros si utilizamos una solución probada, en vez de una forma que se nos ocurrió a nosotros.

Como vemos, excepto quizás en aplicaciones muy chicas y simples, las desventajas superan ampliamente a los beneficios que nos ofrece este método. Además, al utilizar Identity, se superan todas las desventajas que consideramos, ya que es una solución conocida, probada y extensible.

Dependencias a incluir para el uso de IdentityPara poder utilizar Identity, debemos incluir algunos paquetes NuGet extra a nuestro proyecto existente. Los paquetes a incluir6 son:

● Microsoft.AspNet.Identity.Owin● Microsoft.AspNet.Identity.EntityFramework● Microsoft.Owin.Host.SystemWeb● Microsoft.Owin.Security.Facebook

Reemplazo del contexto de la base de datosLa primera modificación que hay que realizar tiene que ver con el contexto de la base de datos (en la aplicación de ejemplo, la clase DataModel). Esta clase existe porque estamos utilizando Entity Framework, y representa la capa de acceso a datos. Para incluir las funcionalidades de Identity, debemos derivar de IdentityDbContext, en lugar de DbContext.

Nótese que, desde ya, estamos usando clases que dependen de Entity Framework (IdentityDbContext).

La clase IdentityDbContext tiene un tipo genérico TUser : IdentityUser. Esto representa al tipo (clase, interfaz o struct) con el que vamos a representar a los usuarios de nuestra aplicación.

class IdentityDbContext<TUser> : ... where TUser :

6 Para instalar un paquete NuGet puede utilizarse el administrador visual de paquetes NuGet de Visual Studio, o ejecutar el comando Install-Package <nombre> en la consola del administrador de paquetes. Las dependencias de los mismos se resolverán e instalarán automáticamente.

13

IdentityUser

Para representar a los usuarios, vamos a crear una clase adicional, AppUser. Esto realmente no es necesario si no incluimos ningún dato o comportamiento adicional del que nos provee IdentityUser (podriamos usar IdentityUser directamente), pero lo hacemos para que si, a futuro, se desean asociar nuevos datos o incorporar comportamiento nuevo a los usuarios, pueda hacerse de forma directa.

public class AppUser : IdentityUser { public string Nombre { get { return UserName; }

set { UserName = value; } } }

Luego, la definición de DataModel queda así:

public class DataModel : IdentityDbContext<AppUser>

Podemos ejecutar las migraciones de Entity Framework para que se generen las tablas necesarias en nuestra base de datos. Luego de hacer esto, si inspeccionamos la base de datos veremos que las tablas generadas se corresponden con lo que vimos en la sección del modelo de datos de Identity (y, además, se agrega la columna Nombre, que se corresponde con la propiedad Nombre que definimos en AppUser).

14

Configuración de Owin

Para configurar OWIN, tenemos que agregar una clase nueva (en nuestro caso, la llamamos IdentityConfig) para realizar ahí la configuración. Luego, simplemente debemos agregar una referencia a la misma en Web.config. En nuestro caso:

Web.config...<appSettings> ... <add key="owin:AppStartup" value="TP3Distribuidas.IdentityConfig" /></appSettings>...

En la clase IdentityConfig, creamos un método Configuration(:IAppBuilder), que será invocado al inicio de la aplicación, para realizar la configuración de OWIN:

public class IdentityConfig{ public void Configuration(IAppBuilder app) {

15

//... }}

De ahora en más, usaremos IdentityConfig>>Configuration para referirnos a este método.

Cuando utilizamos OWIN, tenemos a nuestra disposición un OwinContext, que es una realización del patrón de diseño Context7. Una de las responsabilidades de OwinContext es ser un punto de acceso para la obtención de objetos de tipos conocidos, que se registraron anteriormente. IAppBuilder define métodos CreatePerOwinContext, que registran callbacks8 que son invocados cuando se necesitan crear instancias de esos tipos, para almacenarlas luego en el OwinContext. Las instancias pueden obtenerse al utilizar el método Get de OwinContext.

Para empezar, en IdentityConfig>>Configuration vamos a registrar (usando el AppBuilder) a nuestro DataModel, para poder obtenerlo luego usando owinContext.Get<DataModel>(). Para esto, debemos invocar una de las versiones del método CreatePerOwinContext, pasándole como parámetro un callback que cree objetos de tipo DataModel. La versión de CreatePerOwinContext que usaremos dependerá si necesitamos o no el OwinContext para la creación de los objetos. En este caso no lo necesitamos, entonces utilizamos la versión que no incluye el tipo genérico (usaremos la otra más adelante). Para especificar el callback, podemos utilizar un lambda, o pasar una referencia a un método. Así quedaría en el primer caso:

IdentityConfig>>Configuration app.CreatePerOwinContext(() => new DataModel());

Como queremos controlar mejor la creación de nuestro DataModel, vamos a definir un método estático Create():DataModel en DataModel9 (que devuelva la instancia de DataModel correspondiente a la solicitud actual), y pasamos una referencia a ese método:

IdentityConfig>>Configuration app.CreatePerOwinContext(DataModel.Create);

7 Para más información sobre el patrón de diseño Context, se puede consultar http://stackoverflow.com/questions/771983/what-is-context-object-design-pattern8 Para más información sobre qué son los callbacks, puede consultarse https://es.wikipedia.org/wiki/Callback_(informática) 9 Este método es un Factory Method, utilizado para crear o proporcionar instancias de DataModel

16

Creación del UserManagerA continuación, vamos a crear un UserManager personalizado para nuestra aplicación, al cual le llamaremos (por nada en especial) AppUserManager. Luego, lo registramos utilizando la versión de CreatePerOwinContext que incluye el tipo genérico, ya que esta vez sí vamos a necesitar el OwinContext.

IdentityConfig>>Configuration app.CreatePerOwinContext<AppUserManager>(AppUserManager.Create);

Seguimos el mismo patrón que en el caso del DataModel, solo que esta vez el Factory Method estático Create sí recibe parámetros. Dentro de este método, creamos y configuramos10 al UserManager:

public class AppUserManager : UserManager<AppUser>{ public AppUserManager(IUserStore<AppUser> store) : base(store) { }

public static AppUserManager Create (IdentityFactoryOptions<AppUserManager> options, IOwinContext context) { // Creamos el AppUserManager var manager = new AppUserManager( new UserStore<AppUser>( context.Get<DataModel>() ) );

// Lógica de validación de nombres de usuario manager.UserValidator = new UserValidator<AppUser>(manager) { AllowOnlyAlphanumericUserNames = true, RequireUniqueEmail = false };

// Lógica de validación de contraseñas manager.PasswordValidator = new PasswordValidator { RequiredLength = 4, RequireNonLetterOrDigit = false, RequireDigit = false, RequireLowercase = false,

10 Creemos que la configuración del UserManager (con el UserValidator, etc) se entiende directamente desde el código, por eso no se incluye ninguna explicación adicional. Vale aclarar que toda esa configuración es opcional.

17

RequireUppercase = false };

// Lógica de bloqueo de usuarios cuando // inician sesión de forma incorrecta manager.UserLockoutEnabledByDefault = true; manager.MaxFailedAccessAttemptsBeforeLockout = 5; manager.DefaultAccountLockoutTimeSpan = TimeSpan.FromMinutes(5);

return manager; }}

Nótese cómo, para la creación del AppUserManager, fue necesario crear una UserStore, y a su vez vincularla con el DataModel11 (que se obtuvo usando el método Get del OwinContext12). Esto tiene que ver con la arquitectura de Identity, que vimos anteriormente.

Registro de un RoleManager y de un SignInManagerComo más adelante veremos el uso de los roles, vamos a registrar un RoleManager de manera análoga a como lo hicimos con los demás componentes, esta vez utilizando un lambda:

IdentityConfig>>Configurationapp.CreatePerOwinContext<RoleManager<IdentityRole>>( (options, context) => new RoleManager<IdentityRole>( new RoleStore<IdentityRole>( context.Get<DataModel>() ) ));

11 Notamos que Identity aplica aquí el concepto de inyección de dependencias, al requerir que pasemos las dependencias en el momento de creación del objeto, en lugar de que se obtengan de alguna otra manera o creen dentro del objeto.12 Podemos hacer esto porque anteriormente ya registramos el DataModel en el OwinContext, cuando usamos el método CreatePerOwinContext.

18

Vemos que, en este caso, decidimos usar directamente la clase RoleManager, e IdentityRole para representar a los roles dentro de nuestra aplicación.

A continuación, registramos un SignInManager. Especificamos que estamos usando el tipo AppUser para nuestros usuarios, y que la clave será de tipo string. Utilizamos el OwinContext para obtener el AppUserManager que registramos anteriormente, y también para obtener un AuthenticationManager, que es provisto de manera automática.

IdentityConfig>>Configurationapp.CreatePerOwinContext<SignInManager<AppUser, string>>( (options, context) => new SignInManager<AppUser, string>( context.GetUserManager<AppUserManager>(), context.Authentication ));

Inclusión de los componentes middleware de seguridadAhora estamos listos para reemplazar el método de autenticación de nuestra aplicación por Identity. Para ello, debemos indicar que queremos utilizar el middleware de autenticación basada en cookies. Esto se logra al usar los métodos Use, del AppBuilder:

IdentityConfig>>Configurationapp.UseCookieAuthentication(new CookieAuthenticationOptions{ AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, LoginPath = new PathString("/Usuarios/IniciarSesion"),});

Aprovechamos para configurar las opciones de la autenticación mediante cookies, como el tipo de autenticación, y la ruta del formulario de inicio de sesión (para cuando se redireccione al usuario, en caso de no estar autorizado).

Modificación de las acciones en UsuariosControllerYa teniendo registrado el middleware para utilizar autenticación mediante cookies, vamos a editar las acciones para registrar a los usuarios e iniciar sesión. Antes que nada, obtenemos referencias al AppUserManager, SignInManager y AuthenticationManager, desde el OwinContext, guardandolas como campos en el UsuariosController:

protected override void OnActionExecuting(...){ base.OnActionExecuting(...);

var ctx = HttpContext.GetOwinContext(); userManager = ctx.GetUserManager<AppUserManager>(); signInManager = ctx.GetUserManager<SignInManager<AppUser, string>>(); authManager = ctx.Authentication;}

19

Importante: vamos a usar estos campos en las acciones que se describirán a continuación.

Inicio de sesiónLos datos para el inicio de sesión se procesan en el signInManager, el que se encarga de iniciar la sesión si las credenciales son válidas.

[HttpPost]public async Task<ActionResult> IniciarSesion(string nombre, string contraseña){ var resultado = await signInManager.PasswordSignInAsync( nombre, contraseña, isPersistent: true, shouldLockout: true ); return AccionParaResultadoDeLogin(resultado);}

El parámetro isPersistent se utiliza para la implementar la funcionalidad de “Recordarme” o mantener iniciada la sesión. shouldLockout define si se cuentan o no los intentos fallidos de inicio de sesión, para bloquear al usuario una vez que se exceda el número de intentos máximo (que se definió en el AppUserManager).

El método AccionParaResultadoDeLogin determina lo que se hará según el resultado del inicio de sesión:

private ActionResult AccionParaResultadoDeLogin(SignInStatus resultado){ switch (resultado) { case SignInStatus.Success: return RedirectToAction("Index", "Tareas"); case SignInStatus.LockedOut: ModelState.AddModelError("FalloInicioSesion", "Has superado el número máximo de intentos para el inicio de sesión"); return View(); case SignInStatus.Failure: default: ModelState.AddModelError("FalloInicioSesion", "Los datos ingresados no se corresponden con los de ningún usuario registrado en el sistema"); return View(); }}

20

Para determinar si se muestra o no el formulario de inicio de sesión, se pregunta si el usuario ya está autenticado, mediante el método Request.IsAuthenticated:

[HttpGet]public ActionResult IniciarSesion(){ if (Request.IsAuthenticated) { return RedirectToAction("Index", "Tareas"); }

return View();}

Registro de UsuariosPara el registro de los usuarios, utilizaremos (como era de esperarse) el userManager. Luego, dependiendo si la creación del usuario fué exitosa, agregamos los errores para que se muestren en la vista, o invocamos la acción de inicio de sesión (para que, al registrarse como usuario en forma exitosa, se inicie la sesión automáticamente).

[HttpPost]public async Task<ActionResult> Registrarse(string nombre, string contraseña){ var nuevoUsuario = new AppUser { Nombre = nombre };

var resultado = await userManager.CreateAsync( nuevoUsuario, contraseña );

if (!resultado.Succeeded) { foreach (var error in resultado.Errors) { ModelState.AddModelError("", error); } return View("IniciarSesion"); }

return await IniciarSesion(nombre, contraseña);}

21

Cierre de SesiónPara el cierre de sesión utilizamos el authManager. De todas las acciones, esta es la más sencilla.

public ActionResult CerrarSesion(){ authManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie); return RedirectToAction("IniciarSesion");}

Agregando funciones de autorizaciónHabiendo hecho todo esto, nuestra aplicación ya tiene las funciones de autenticación local de Identity. Para agregar funciones de autorización, se utilizan atributos o anotaciones13. Se agrega la anotación [Authorize] a las acciones que no se pueden ejecutar, a menos que se haya iniciado sesión.

[Authorize]public ActionResult SoloSiSeInicióSesion()...

En el caso que todas o la mayoría de las acciones de un controlador necesiten autorización para ser ejecutadas, se puede agregar la anotación [Authorize] al controlador en sí, marcando las acciones excepcionales con [AllowAnonymous].

[Authorize]public class TareasController : Controller{ public ActionResult SoloSiSeInicióSesion() ... [AllowAnonymous] public ActionResult PuedeRealizarseSinIniciarSesión() ...}

Autorización mediante rolesLa autorización mediante roles se realiza con el agregado del parámetro Roles a la anotación [Authorize]:

[Authorize(Roles = "admin")]public ActionResult SoloAdministradores()...

13 Si bien en C# se denominan oficialmente atributos, el concepto de atributo ya se utiliza (habitualmente) para referirse a lo que C# denomina campos en los objetos. Por esta razón, a partir de este momento se va a usar el término anotación, que representa el concepto equivalente en Java.

22

Para gestionar los roles se utiliza el RoleManager. El siguiente código crea y guarda el rol “admin”, en caso de que no exista. Obtenemos el RoleManager desde el OwinContext.

var ctx = HttpContext.GetOwinContext();var roleManager = ctx.Get<RoleManager<AppRole>>();if (!roleManager.RoleExists("admin")){ roleManager.Create(new AppRole("admin"));}

Para agregar un rol a un usuario, se utiliza el userManager, de la siguiente manera:

userManager.AddToRole(idUsuario, "admin");

Y, en el caso particular del usuario actual:

userManager.AddToRole(User.Identity.GetUserId(), "admin");

Agregado de la autenticación remota mediante FacebookLlegó el momento de agregar la función de autenticación remota a nuestra aplicación. Elegimos a Facebook como ejemplo de proveedor para el inicio de sesión, pero pueden usarse otros (como Twitter o Google). Como Facebook recibirá y responderá las solicitudes de nuestra aplicación, como paso previo debemos registrarla en Facebook.

Registro de la aplicación en FacebookPodemos registrar nuestra aplicación en Facebook Developers (developers.facebook.com/apps). Se nos solicitará el ingreso de información sobre nuestra aplicación.

23

Al finalizar el proceso, Facebook nos asignará un ID de aplicación, y una clave secreta. Vamos a usar esos dos datos para poder establecer la comunicación entre nuestra aplicación y los servidores de Facebook.

Inclusión de los componentes de middleware de seguridadSe deben agregar los middleware de seguridad correspondientes, agregando el ID de aplicación y el app secret proporcionados por Facebook:

IdentityConfig>>Configurationapp.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);app.UseFacebookAuthentication( appId: "908402159277416", appSecret: "...");

Modificación de la vistaAgregando el siguiente código a la vista de inicio de sesión, mostraremos un botón por cada proveedor externo de inicio de sesión que hayamos registrado. Para obtener los proveedores registrados usamos el OwinContext.

@using (Html.BeginForm("LoginExterno", "Usuarios")){ @{ var loginProviders = Context.GetOwinContext() .Authentication.GetExternalAuthenticationTypes(); } @foreach (AuthenticationDescription p in loginProviders) { <button type="submit" id="@p.AuthenticationType" name="provider" value="@p.AuthenticationType" title="Inicie sesión con su cuenta de @p.Caption"> Iniciar Sesión con @p.AuthenticationType </button> }}

Nótese que enviamos el formulario a la acción LoginExterno del controlador Usuarios. Por lo tanto, tenemos que implementarla.

24

Acciones nuevas en UsuariosControllerLas acciones que deberán implementarse se corresponden con los pasos a seguir para iniciar sesión con OAuth, que vimos anteriormente:

LoginExternoSegún lo que acabamos de agregar en la vista, la acción LoginExterno va a recibir un provider. Lo que tenemos que hacer es solicitar permiso al mismo para redireccionar al usuario, brindándole una referencia adicional que sirva como callback, para que el provider pueda invocar a nuestra aplicación más adelante (después de que el usuario inicie sesión).

[HttpPost]public ActionResult LoginExterno(string provider){ // Solicitar redireccionamiento al proveedor // de inicio de sesión externo return new ChallengeResult( provider, Url.Action("CallbackLoginExterno", "Usuarios") );}

Vemos que definimos como callback a la acción CallbackLoginExterno, en UsuariosController. El objeto ChallengeResult que devolvemos representa un resultado http. Desde nuestro punto de vista, toda la funcionalidad necesaria ya está implementada en HttpUnauthorizedResult, por lo que utilizamos a ChallengeResult casi como un Command14, que nos permite ejecutar el siguiente código al procesar el resultado de la acción15:

14 Véase en.wikipedia.org/wiki/Command_pattern para aprender sobre el patrón de diseño Command.15 Como se verá más adelante, para esto usamos el método ExecuteResult.

25

// La uri que pasamos en el constructor de ChallengeResultvar uriRedireccion = Url.Action("CallbackLoginExterno", "Usuarios");

var propiedades = new AuthenticationProperties { RedirectUri = uriRedireccion};

// Obtenemos el provider desde el constructor de ChallengeResultauthManager.Challenge(propiedades, provider);

Recordemos que authManager = HttpContext.GetOwinContext().Authentication, donde HttpContext es una propiedad del controlador. El método Challenge provoca que el middleware de seguridad invoque al servicio para realizar la autenticación. Esta invocación varía según el proveedor (y está definida en el middleware correspondiente).

Para ejecutar este código al procesar el resultado de la acción, usamos el método ExecuteResult (que es un método abstracto definido en la clase ActionResult), que nos permite hacer justamente eso, brindándonos el contexto en el que se está ejecutando el resultado. Guardamos los datos que obtuvimos desde el constructor, y reobtenemos el authManager desde el contexto que se nos pasa. El código completo de la clase ChallengeResult se presenta a continuación:

internal class ChallengeResult : HttpUnauthorizedResult{ public ChallengeResult(string provider, string redirectUri) { Provider = provider; UriRedireccion = redirectUri; }

public string Provider { get; set; } public string UriRedireccion { get; set; }

public override void ExecuteResult(ControllerContext context) { var propiedades = new AuthenticationProperties { RedirectUri = UriRedireccion }; context.HttpContext.GetOwinContext().Authentication .Challenge(propiedades, Provider); }}

Después de hacer esto, la autenticación está funcionando hasta antes de ejecutar el callback (representado por la acción CallbackLoginExterno, que todavía no hemos implementado).

Si intentamos iniciar sesión ahora, vemos que se cumple lo que habíamos dicho cuando hablamos de OAuth, nuestra aplicación solicita una redirección, y somos redirigidos hacia la página de Facebook, donde iniciamos sesión y autorizamos a la aplicación.

26

Notamos que, como ya habíamos mencionado, el usuario inicia sesión en forma directa con Facebook (luego de la redirección, la url es la de Facebook), por lo que las credenciales del mismo para el acceso a Facebook nunca son compartidas con nuestra aplicación.

CallbackLoginExternoA continuación, mostramos la implementación de la acción CallbackLoginExterno. El código está explicado mediante comentarios:

public async Task<ActionResult> CallbackLoginExterno(){ // Usamos el authManager para obtener los datos del login externo var infoLogin = await authManager.GetExternalLoginInfoAsync(); // Si no se realizó el login externo, redireccionamos if (infoLogin == null) { return RedirectToAction("IniciarSesion"); }

// Si el usuario ya existe en nuestro sistema, iniciar sesión var resultado = await signInManager.ExternalSignInAsync( infoLogin, isPersistent: false ); // Si el usuario todavía no existe en nuestro sistema, (falla // el inicio de sesión), lo creamos (y retornamos) if (SignInStatus.Failure.Equals(resultado)) { return await CrearUsuarioConDatosExternos(infoLogin); }

27

// Esto se ejecuta solo si no falló el inicio de sesión. El // método es el mismo que usamos para el inicio de sesión // convencional. return AccionParaResultadoDeLogin(resultado);}

El método CrearUsuarioConDatosExternos:

private async Task<ActionResult> CrearUsuarioConDatosExternos(ExternalLoginInfo infoLogin){ // Creamos el usuario con los datos obtenidos del proveedor de // inicio de sesión externo var usuario = new AppUser { UserName = infoLogin.DefaultUserName }; var res = await userManager.CreateAsync(usuario); if (!res.Succeeded) { return VistaConErrores(res); }

// Vinculamos al usuario con su login remoto // (recordar la tabla AspNetUserLogins) res = await userManager.AddLoginAsync(usuario.Id, infoLogin.Login); if (!res.Succeeded) { return VistaConErrores(res); }

// Iniciamos la sesión await signInManager.SignInAsync( usuario, isPersistent: false, rememberBrowser: false ); return RedirectToAction("Index", "Tareas");}

private ActionResult VistaConErrores(IdentityResult res){ // Agregar los errores para mostrarlos en la vista foreach (var error in res.Errors) { ModelState.AddModelError("", error); } return View();}

Luego de esto, ¡Ya hemos terminado la implementación del inicio de sesión remoto mediante Facebook! :D

28

ConclusionesEl problema de la autenticación y autorización está presente en la gran mayoría de las aplicaciones web. El uso de Identity para la implementación de la autenticación, autorización y gestión de usuarios proporciona muchas ventajas, como hemos discutido en el desarrollo del trabajo. Conocer esta tecnología nos da una facilidad extra, además, para entender el código de otros proyectos que también hagan uso de la misma.

En este trabajo se realizó una investigación que cubre el tema de Identity con cierta profundidad. La mayoría de los recursos que se encuentran en la web no hacen un análisis tan profundo de la tecnología, concentrándose más en cómo utilizar y personalizar la plantilla para aplicaciones ASP.NET con autenticación de usuarios, ofrecida por Visual Studio. Si bien esto es útil, no nos ayuda a entender bien cómo funciona Identity (porque usualmente no se explica el código que ya viene incluido, o solo se lo hace en líneas muy generales), y tampoco es útil si queremos agregar esta tecnología a un proyecto existente (la plantilla solo sirve para proyectos nuevos).

Algunos temas que no se cubrieron en el trabajo, que pueden servir como punto de partida para continuar la investigación sobre el tema, son:

● Uso de los Claims● Componentes de Identity además de los usados para Entity Framework● Arquitectura de las aplicaciones OWIN● Envío de mails de confirmación● Técnicas de autenticación por dos factores

Este trabajo se realizó para la materia Programación de Aplicaciones Distribuidas, dictada en la UTN-FRT. En su desarrollo se encuentran integrados contenidos de muchas materias de la carrera, además de Programación de Aplicaciones Distribuidas, principalmente Diseño de Sistemas, Análisis de Sistemas, Gestión de Datos, Programación Web y Paradigmas de Programación.

Espero que este trabajo haya sido útil y de fácil lectura.

Referencias[1]. http://www.asp.net/identity/overview/getting-started/introduction-to-aspnet-identity

[2]. https://mva.microsoft.com/en-us/training-courses/customizing-aspnet-authentication-with-identity-8647

[3]. https://en.wikipedia.org/wiki/OAuth

[4]. http://www.elladodelmal.com/2014/01/owin-y-katana-como-crear-apps-aspnet.html

[5]. http://stackoverflow.com/questions/31960433/adding-asp-net-mvc5-identity-authentication-to-an-existing- project

[6]. https://coding.abel.nu/2014/05/whats-this-owin-stuff-about/

29