ASP .NET Core Identity en Cosmos DB

La mejor forma de manejar la autorización y autenticación dentro de una aplicación web con ASP .NET Core es por medio de ASP.NET Core Identity. Es la evolución de lo que se conocía como Membership Provider en ASP .NET.

Entre otras mejoras Identity permite interactuar con múltiples protocolos de autorización y autenticación sobre HTTP.

Dentro del flujo de autorización es necesario interactuar con el sistema de persistencia que tenemos implementado en nuestra aplicación web que naturalmente estará basado en usuarios y roles. La implementacion mas común que veremos de Identity es ocupar una base SQL (SQL Server/Azure) con Entity Framework.

Haremos una implementación de este proveedor de identidad para que se integre con Cosmos DB.

Identity Core 2.0

Solo hagamos un breve repaso de los objetos que expone el proveedor (Microsoft.AspNet.Identity) que ocuparemos para nuestra implementación:

UserManager<TUser>: Esta clase principalmente tiene la responsabilidad de controlar todos los atributos del usuario contra el motor de persistencia, esta clase tiene una dependencia con IUserStore<TUser>. Que asume deberá ser inyectada en el constructor.

IUserStore<TUser>: Esta interfaz contiene todas las firmas con las operaciones básicas (para usuarios) que se deberán ejecutar sobre el proveedor de persistencia.

SignInManager<TUser>: Esta clase tiene la responsabilidad de controlar todo lo que tiene que ver con la autenticacion sobre cualquier protocolo, es la encargada de determinar si el usuario tiene los atributos para autenticarse de forma exitosa en la aplicación y también es la encargada de establecer la sesión sobre el protocolo que se determine (Cookies, Issued Tokens, JWT). Tiene dependencia con UserManager<TUser>.

RoleManager<TRole>: Cuando nuestra aplicación requiere roles, esta clase tiene la responsabilidad de controlar todos los atributos de los roles contra el motor de persistencia. Tiene dependencia con IRoleStore<TRole>.

La mayoría de estas clases e interfaces son genéricas pues permiten establecer una entidad POCO para definir las propiedades del usuario y rol.

Existen mas clases dentro del proveedor pero por ahora solo ocuparemos las mencionadas.

ASP .NET Identity Core – Cosmos DB

Integrando Cosmos DB

Lo primero que tenemos que hacer es implementar nuestra propia versión de IUserStore<TUser> y de IRoleStore<TRole>. Para esto tambien requerimos definir nuestra entidad que representa al usuario, que se veria algo asi:

En el código podemos ver que estamos heredando de IdentityUser<TKey> que nos permite tomar propiedades comunes para nuestra entidad como: Id, nombre de usuario, correo, email, contraseña, etc. No es obligatorio hacer esta herencia, podríamos omitirlo y nosotros establecer las propiedades que necesitemos. Unas propiedades si las necesitaremos de forma predefinida. Ya lo veremos

También tenemos una propiedad con el nombre de Tenan,  esta se ocupa en el caso donde queramos tener una partición en la colección de usuarios para la base de Cosmos DB. Por ahora esta en duro pues solo es para ejemplificar.

La implementacion de IUserStore<TUser> se podría referir a nuestro repositorio de usuarios dentro de nuestra aplicación, así es que lo que vamos a hacer es incluir la implementacion de esa interfaz en nuestra misma interfaz que define nuestro de repositorio de usuarios.

La definición del repositorio de usuario se podría ver así:

Estoy definiendo mi propia interfaz de IUserRepository que expone 4 métodos propios y adicional esta interfaz debe implementar IUserStore<User>. Adicional estoy agregando otras interfaces a implementar.

IUserPasswordStore<User>: Esta interfaz se requiere para definir como se obtendrá la propiedad que contiene la contraseña con hash. Es importante pues se ocupa a momento de autenticar por medio de contraseña al usuario.

IUserRoleStore<User>: Esta interfaz se requiere para controlar como se asocian los roles a los usuarios. Esto es porque en nuestra aplicación esta basada en usuarios y roles. Si no requerimos roles se puede omitir.

La clase de la implementacion de IUserRepository queda bastante extensa pero se veria algo asi:

Hasta este punto no hemos escrito aun nada de código que interactúe con Cosmos DB. En la clase UserRepository podemos ver que hereda de BaseRepositoryCosmos<User> que es donde se hace la interacción con Cosmos DB. La clase base  BaseRepositoryCosmos<User> encapsula la lógica necesaria para crear el cliente, crear la base y la colección basada en la entidad genérica que recibe; en este caso nuestra entidad User, internamente la clase base define el nombre de la entidad como el nombre de la colección para Cosmos DB.

Esta es la definición de BaseRepositoryCosmos<User>:

Para mas detalle de esta clase base para el repositorio lo explico aquí.

Como ya lo explicamos, esto nos permite interactuar con la base de Cosmos DB. Solo nos queda implementar cada unos de los métodos que requieren las interfaces que ocuparemos.

Aqui la implementacion de algunos metodos:

Los métodos FindByIdAsync FindByNameAsync como su nombre lo dice permiten buscar al usuario por id o por username, estos métodos los ocupa bastante el proveedor de Identity, pues hay varias operaciones donde tiene que ir a buscar a un usuario en especifico.

En el código de arriba podemos ver los métodos para crear y actualizar, ambos internamente ocupan un metodo de la clase base que es Upsert, ese método ejecuta la operación de UpsertDocumentAsync.

En este enlace de la solución se puede ver la implementación completa de cada método. Solución aquí.

Una vez que tenemos la implementación completa en nuestros repositorios hay que indicarle al proveedor de donde debe tomar lo que requiere.

Configurando proyecto Web (ASP .NET Core)

El proveedor de Identity se configura y se inicializa en nuestro proyecto web, aquí es donde establecemos todas las políticas y protocolos con los que el sitio web autoriza el acceso.

Previo debemos indicar a los objetos de UserManager<User> SignInManager<User> que deben de ocupar nuestro repositorio de usuario pues ahí esta la lógica para consumir nuestra colección de Cosmos DB. Para eso tenemos que hacer nuestra propia versión heredando de cada clase.

Se veria algo asi:

Aquí tenemos la clase ApplicationUserManager  que hereda de UserManager<User> y vemos que la definición del constructor requiere de muchas dependencias. La que nos interesa es la primera, ahí es donde le indicamos que debe de utilizar nuestro repositorio definido sobre la interfaz IUserRepository.

Las demás dependencias las dejamos igual, internamente el motor de inyección de dependencias se encargara de resolverlas.

Ahora vemos la clase ApplicationSignInManager que hereda de SignInManager<User> y realizamos lo mismo en el constructor. Esta clase tiene dependencia con UserManager<User> así es que le indicamos que la clase que debe ocupar es nuestra definición de ApplicationUserManager.

Con esto logramos indicar al proveedor que debe de ocupar nuestro repositorio que se conecta a Cosmos DB.

Ahora debemos de indicar al motor de inyección de dependencias de ASP .NET Core el registro y configuración de todas nuestras dependencias. Esto lo hacemos en el archivo Startup.cs como naturalmente se hace.

Primero indicamos que nuestro método sera por medio de Cookies.

Hacemos el registro de todas nuestras dependencias. También indicamos como resuelve la configuración para Cosmos DB con al clase CosmosConfiguration.

Finalmente le indicamos al proveedor Identity las dependencias que debe de ocupar.

Controlador y Login

Ahora ya podemos ocupar el proveedor de forma natural sobre nuestros controladores para determinar la autenticación y autorización. Aquí tenemos el controlador que ocupamos en la vista de login.

Podemos ver como en el constructor del controlador inyectamos la dependencias de UserManager<User>SignInManager<User>. En la acción DoLogin es donde autenticamos al usuario obteniendo sus credenciales (nombre de usuario y contraseña), internamente ocupamos el método PasswordSignInAsync que recibe el nombre de usuario y contraseña.

Internamente el método PasswordSignInAsync se encarga de hacer las validaciones necesarias para determinar si autentica al usuario ocupando nuestro repositorio. Por eso es importante hacer la implementación completa de IUserStore<User> pues de forma indistinta ocupa cada unos de los métodos.

En el caso de que queramos restringir un recurso basado en roles podemos ocupar el atributo Authorize:

Internamente el proveedor de Identity ocupa nuestro repositorio para determinar si el usuario tiene el rol de “Admin”. Para esto debemos hacer la implementación correcta de IUserRoleStore<User>.

De forma básica esto es lo que se tendría que hacer para integrar el proveedor con Cosmos DB. Aquí dejo una vista de la solución en visual studio con todo lo que contiene. Y el enlace a la solucion en github.

 

Migrando desde SQL Server hacia Cosmos DB (Migration Tool)

Existe una herramienta desarrollada por el equipo de Microsoft Azure para realizar migracion de datos hacia Azure Cosmos DB. Esta herramienta permite importar datos desde varias fuentes como: JSON files, CSV files, SQL, MongoDB, Azure Table storage, Amazon DynamoDB, e incluso Azure Cosmos DB SQL API.

Esta vez realizaremos la importación desde SQL Server y validaremos como establece las relaciones de las tablas o que opciones tenemos para controlar la forma en la que importamos los datos.

Primero vamos revisar nuestros objetos/tablas (esquema BD) en SQL Server, después ejecutaremos la herramienta y veremos como resuelve cada objeto, y como resuelve los campos con su tipo de dato.

Migration Tool

 

SQL Server (source)

Como fuente de datos vamos a usar la tabla DimProduct de la clasica BD de AdventureWorksDW. La tabla contiene:

DimProduct – AdeventureWorksDW

Como podemos ver en la imagen tenemos suficientes tipos de datos para probar. Tenemos incluso el tipo varbinary.

Nuestra llave primaria en esta caso esta sobre el campo ProductKey esto debemos tenerlo en cuenta al momento de la migración. La herramienta nos permite establecerlo.

Cosmos DB (target)

Para el destino debemos habilitar el servicio de Cosmos DB, lo podemos hacer hacia el emulador de Cosmos DB (lo puede descargar aqui) o podemos crear el servicio desde el portal de Azure.

Si tenemos el emulador se debería de abrir una pagina en localhost que se ve asi:

Azure Cosmos DB Emulator

Como se puede ver en la imagen no existe ninguna DB y ninguna colección. De esta parte solo debemos de validar que podamos ejecutar el emulador correctamente y recuperar los datos para la conexión.

En el caso del emulador los parámetros de conexión son:

Estos parámetros son los predefinidos por el emulador. Podemos cambiarlos ejecutando el emulador desde la consola.

Migration Tool

La herramienta de migración la podemos descargar de aquí.

Ejecutamos la herramienta en modo UI (dtui.exe) y se abrirá la siguiente ventana:

Migration Tool – Source

Y lo primero que debemos indicar es la fuente de datos. En la imagen podemos ver todas las fuentes de datos que podemos ocupar. En este caso vamos a ocupar SQL para conectarnos a la base de datos de AdventureWorksDW.

Al momento de seleccionar la fuente como SQL debemos de configurar la conexión hacia el servidor y base de datos con una cadena de conexión, y adicional debemos definir un query con el que obtiene los registros para migrar. Esto facilita bastante porque incluso podríamos hacer un join entre distintas tablas para formar una sola entidad/documento.

Migration Tool – Source SQL

Nos permite escribir directamente un query o seleccionar un archivo que lo contenga, este es mi query:

Cada una de las columnas que incluye la sentencia se creara como propiedad dentro de la entidad/documento dentro de Cosmos DB con el mismo nombre.

Le damos siguiente. Y ahora nos solicita configurar el destino, que en nuestro caso sera Cosmos DB. Debemos indicar los parámetros de conexión, y los datos de la base de datos y nombre de la colección. En caso de que no exista alguno lo creara.

Migration Tool – Target

Veamos cada uno de los parámetros que estamos estableciendo:

Export to: Aquí nos permite establecer la importación de forma secuencial definiendo el valor de partición.

Connection String: Definimos los parámetros de conexión hacia Cosmos DB, en nuestro caso estamos ocupando el emulador local. Y la cadena queda separada por “;”.

Aquí también estoy definiendo el nombre de la base de datos como: migrationdb

Collection: El nombre de la colección, si no existe la crea.

Partition Key: En este caso vamos manejar las particiones de forma ilimitada (unlimited auto-scaling partitioning) por lo tanto debemos definir nuestra llave de partición. Establecemos la llave de partición la linea del producto que se encuentra en la propiedad ProductLine. La sintaxis establece que se debe indicar con “/” esto es porque podríamos seleccionar una propiedad que se encuentra a mayor profundidad en nuestro objeto. Ej. “/prop1/deep1/deep2”

Collection Throughput: Para poder implementar el auto-particionado o tener nuestro indice en forma ilimitada lo mínimo que podemos establecer como RU (request unit) son 1,000 o mas.

Id Field: Aquí definimos la propiedad que funcionara como id único, en nuestro caso ocupamos la propiedad ProductKey.

Le damos siguiente. Podemos establecer un archivo de log.

Finalmente ejecutamos la importación.

Migration Tool – Results

Como podemos ver la imagen finalizo correctamente la importacion de los 606 registros. Ahora vamos a ver que hay en el explorador del emulador:

Azure Cosmos DB Emulator

Vemos que hizo la creación de la base de datos y colección. También podemos ver que hizo la inserción de los registros correctamente.

Ahora vamos a ver como resolvió el tipo varbinary:

Varbinary field

El campo LargePhoto es varbinary y vemos que lo resolvió como un arreglo de enteros (no es buena idea guardar binarios sobre un indice en Comos DB). Vemos que la codificación de caracteres también lo resolvió de forma correcta:

Encoding

Es de gran utilidad esta herramienta incluso cuando cambiamos la forma de particionar del indice y tenemos que migrar los datos.

Troubleshooting: Algunas veces manda un error la conexión hacia el emulador al momento de ejecutar el ultimo. Nada mas reinicia el emulador.