Gestión de identidad online II

Continuación de este articulo sobre Identidad Online

Gestión moderna de identidad online

En el siguiente ejemplo se va a mostrar una solución técnica para gestionar una aplicación web que utiliza distintos proveedores de identidad para iniciar sesión y tiene soporte para varios dispositivos.

clip_image001

Si se piensa en un entorno puramente web, normalmente se utiliza una cookie para almacenar la sesión de un usuario, es decir, un hash opaco que permite identificar al usuario de manera única y que el servidor conoce. Se puede poner como ejemplo Sharepoint Online que utiliza el sistema de autenticación de Office 365 que se apoya en dos cookies llamadas FedAuth y rtFa. De esta manera el sistema sabe que usuario está accediendo al sistema a través de esas dos cookies, además esas cookies están configuradas para utilizas sólo HTTPS y solo poder usarse desde el dominio de origen.

Para el ejemplo que se va usar en este sitio web, se va a utiliza Microsoft Azure Active Directory como gestor de identidad y ASP.NET MVC 5 como framework para desarrollar la solución de la gestión de identidad.

Tradicionalmente en ASP.NET existía un módulo que permitía gestionar roles y miembros de la una web, en la última versión de ASP.NET se utiliza ASP.NET Identity que es un sistema más moderno y permite utilizar directamente Entity Framework como proveedor de persistencia para almacenar los datos. Como EF utiliza un modelo de contextos para gestionar la comunicación con la base de datos en ASP.NET Identity hay un contexto de EF generado que llama IdentityDbContext que ya tiene definidos dos propiedades Users y Roles de tipo IDbSet<T> siendo T el tipo de la clase que maneja las entidades de los usuarios y roles respectivamente.

IdentityDbContext

Esta clase IdentityDbContext hereda de otra clase que tiene cuatro argumentos de tipo: IdentityDbContext<TUser, TRole, TKey, TUserLogin, TUserRole, TUserClaim> donde cada uno de los argumentos define el tipo de la entidad que manejará cada una de las características de la gestión de identidad, usaurios, roles, identidad del usuario, login, usuarios y roles y claims. En ASP.NET MVC vienen directamente una serie de tipos para todos esos artefactos, si se hereda directamente de IdentityDbContext se tienen todos esos tipos de forma predeterminada.

Con esta configuración, se tiene, de forma automática la gestión de los usuarios, los roles de los usuarios, ect directamente. En algunos casos será necesario poder modificar la columnas de esas tablas para contemplar escenarios que el proveedor no soporta de forma predeterminada. Para ello, ya que se está utilizando Entity Framework, la forma adecuada es hacer una clase que herede del tipo que se desea modificar y luego hacer un contexto de Entity Framework que herede de IdentityDbContext de la clase recién creada.

 public class ApplicationUser : IdentityUser
{
    public string DisplayName { get; set; }
    public string ProfileEmail { get; set; }
    public bool HasProfile { get; set; }
    public string Picture { get; set; }
    public DateTime Created { get; set; }
    public string ClaimsProvider { get; set; }
    public virtual ICollection<Room> Rooms { get; set; }
    public virtual ICollection<Session> Sessions { get; set; }
    public Dictionary<Room, DateTime> RoomLastReaded { get; set; }
}

En este ejemplo se está heredando de IdentityUser para extender la clase que gestiona de forma predeterminada a los usuarios. Una vez hecho eso, el siguiente paso es extender el contexto de Entity Framework para especificar este nuevo tipo ApplicationUser.

public class PhotoChatDbContext : IdentityDbContext<ApplicationUser> { }

En el caso de que no se esté usando EntityFramework toda la gestión de podrá hacer exactamente igual generando las consultas necesarias para gestionar las tablas de usuarios, roles y demás artefactos.

Integrando Microsoft Azure Active Directory

Para poder hacer la integración de Azure Active Directory en el proyecto de ASP.NET, cuando se crea el proyecto directamente se puede especificar el tenant de Active Directory y genera el código necesario.

Durante la creación del web site se puede especificar el tipo de autenticación y si se selecciona Work and School Accounts aparece un cuadro de dialogo como este:

clip_image003

Donde se puede especificar el nombre de la cuenta de Azure Active Directory, en mi caso: luisguerrerospain.onmicrosoft.com.

Cuando el proceso de creación de la plantilla termina, en el tenant de Azure se ha creado una aplicación con el mismo nombre que el nombre de proyecto.

clip_image005

Y se han agregado a la configuración del web.config una serie de configuraciones de aplicación que permiten a WIF (Windows Identity Foundation)

clip_image007

Además, en la parte de configuración inicial de la app se asigna Windows Azure Active Directory como proveedor de identidad predeterminado.

clip_image009

A partir de este instante se puede especificar cuál será el método con el cual se gestionarán los inicios de sesión de los usuarios.

Sesión, inicios de sesión y cookies

Cuando un usuario entra por primera vez en un sitio web al que no ha accedido antes el sitio web genera una serie de cookies que le permiten identificar al usuario. El mismo proceso ocurre cuando un usuario inicia sesión, durante el proceso de validación de credenciales se genera una cookie que identifica la sesión de ese usuario y le informa al servidor de que el usuario ha iniciado sesión. Normalmente estas cookies de gestión de sesión tienen una caducidad, y pasado un tiempo, la sesión se caduca y el usuario tiene que volver a iniciar sesión. Pero si uno se fija en cómo funciona algunos sitios web, las credenciales tienen una caducidad mucho mayor. Si se piensa en un ejemplo concreto, Facebook, una vez que se inicia sesión, al menos que pasen muchos meses desde la última vez que se navegó por el sitio web, nuestras credenciales está ahí. ¿Eso significa que Facebook ha mantenido la sesión durante todo ese tiempo? Desde luego que no, pero ha utilizado otro mecanismo para manejar el inicio de sesión.

En vez de asociar la sesión actual del usuario con el inicio de sesión, lo que hace Facebook y otras páginas es definir una cookie que define la identidad de ese dispositivo. Esa identidad que se mantiene durante un periodo de tiempo muy grande, es lo que utiliza el usuario para iniciar sesión. Eso no significa que la cookie contenga el nombre de usuario y contraseña.

Evidencias de seguridad

Cuando un usuario quiere iniciar sesión tiene que proveer al sistema una evidencia de que es el usuario que dice ser, normalmente eso se hace a través del más que famosa combinación de nombre usuario / contraseña. Pero para este mecanismo de seguridad eso no es posible.

En un diseño minimalista de la base de datos para gestionar usuarios, lo mínimo que se tiene es una tabla Users que contiene el nombre de usuario y contraseña:

clip_image010

Pero en este nuevo mecanismo de autenticación de los usuarios hay que generar una cookie con un valor, que permita iniciar sesión en el servidor de manera automática, pero que a la vez sea segura. Para ello el modelo de la base de datos tiene que cambiar para dar cabida a esa lista de identidades de dispositivos.

clip_image011

La tabla de usuarios se mantiene de la misma manera, pero se añade una tabla nueva llamada Sessions donde se almacenan los tokens generados por la aplicación que permite identificar a los usuarios. Esta tabla es realmente donde se almacenan cada una de las credenciales de todos los dispositivos donde el usuario ha iniciado sesión.

Consulta de inicio de sesión

Antes de la inclusión de la tabla Sessions, la consulta para hacer el login podría ser algo como esto:

SELECT Id,Name FROM Users WHERE Username = ‘myusername’ and Password = ‘password’

En el caso de que la consulta devolviera resultados y el valor de la columna name fuera igual que el nombre de usuario, eso significaba que el usuario existía y que la combinación de nombre de usuario / contraseña es válido. Aquí recordad lo que se comentó en otro punto del artículo, nunca se guardan las contraseñas en texto plano.

Pero utilizando la tabla Sessions la cosa cambia. En vez de consultar el par de nombre de usuario / contraseña, lo que se hace es buscar en esa tabla un token concreto, y si se ha encontrado entonces lo que se hace es buscar el usuario asociado a este registro. La forma de autenticar es diferente, ya que implica buscar un valor concreto del token, que puede ser provisto a la web a través de una cookie, cabecera, ect.

Un token tiene este aspecto Y2VhY2RiNzAtMTFhNC00YmZkLWJlNjMtODJlNjg0ZjQ0ZGYwfDYzNTUxOTI0NTc5ODIxMzIwOHxNaWNyb3NvZnQuUGhvdG9DaGF0LlJlcG9zaXRvcnkuU2VjdXJpdHkuQ2xhaW1zLlBob3RvQ2hhdENsYWltRmVkZXJhdGlvblVzZXJNb2R1bGU.

Creando un filtro de autenticación

En este ejemplo concreto que se está usando ASP.NET, se puede utilizar un filtro de autenticación para crear una clase que permita al sistema decirle cuando los usuarios están autenticados o no. Aquí el desarrollador puede elegir el transporte para su identificador de sesión, que puede ser una cookie como se ha especificado antes, pero si se está desarrollando una API que usarán dispositivos móviles también se puede integrar como una cabecera HTTP.

Durante el proceso de inicio de sesión se han utilizado Claims para almacenar la información que el proveedor de identidad ofrecía sobre los usuarios, así que una vez que se comprueba que el usuario es correcto se tienen que volver a leer esa lista de Claims para establecerlas como el usuario ambiental.

clip_image013

Una vez que se ha obtenido la referencia al token, ya venga de una cabecera o de una cookie, el siguiente proceso es buscarlo en la base de datos utilizando esta expresión de Linq.

clip_image014

Lo que se hace es buscar en la colección de usuarios uno tal que, en su lista de sesiones uno de los token sea igual que el token que se pasa por parámetro. Esta es la esencia del proceso de iniciar sesión con un token por dispositivo, que te permite no tener que proveer el nombre de usuario y contraseña, sino que solamente con este token opaco se pueda encontrar a este usuario.

Una vez que se obtiene la referencia al AplicationUser que contiene el usuario, lo que hay que hacer es volver a generar un objeto ClaimsPrincipal que se establecerá en la propiedad del contexto actual de HTTP en Principal. De esta manera el runtime de ASP.NET sabe que un usuario ha iniciado sesión y desde cualquier punto de la ejecución del pipeline de ASP.NET se podrá acceder a esta información del usuario.

clip_image016

En el caso de que no se encuentre el usuario lo que hay que hacer es lanzar una excepción de error de autenticación para avisar al usuario de que no está correctamente autenticado.

Conclusiones

La gestión de la identidad cambia conforme los consumidores cambian de forma de usar las aplicaciones web. Para nuevos desarrollos de aplicaciones esta sería la forma correcta de empezar un proyecto para gestionar la identidad de los usuarios, dejando la libertad de que utilicen el proveedor de identidad que más les convenga. También Windows Azure Active Directory es una pieza central de este diseño, porque es el repositorio central de usuarios que la aplicación utiliza, disponible como servicio y sin necesidad de que se tenga que estar pensando en la escalabilidad del servicio.

Crear esta solución en otro lenguaje sería igual, pero algunas de las partes que ASP.NET ya integra directamente (como por ejemplo el soporte de WS-Fed) tendría que hacerse a mano o utilizar algún otro framework de terceros.

Luis Guerrero.

Technical Evangelist Microsoft Azure.

@guerrerotook