Autenticación de Azure Key Vault con Azure Active Directory

Azure Key Vault es un repositorio/colección de llaves criptograficas (JWKs) que se resguardan sobre Azure. Expone un API para tener acceso y realizar operaciones sobre estas llaves.

La documentación no es clara sobre como tenemos acceso a esta API para hacer la integración con la aplicación que estamos desarrollando.

El control de acceso hacia el API de Azure Key Vault se establece en las políticas de acceso hacia un AAD Identities (Azure AD as a Third-Party Identity). Es una alternativa para autorizar el acceso hacia Azure Key Vault.

Vamos a ver como realizar esto por medio de 2 alternativas con Azure AD:

1. Creando una App dentro de Azure AD

2. Creando un Certificado en Azure AD (preferible)

 

Authenticate Azure Key Vault

Registrando aplicación con Azure AD

Para estro primero debemos ir al tenant de nuestra subscripcion desde el portal de Azure. Y entrar a la opción que dice “Azure Active Directory”.

Aquí seleccionamos crear nueva aplicación:

Se abrirá un formulario que nos pide nombre, tipo de aplicación y la URL de ingreso (Sign-on URL).

Nombre: AzureKeyVaultApp

Tipo Aplicación: Web app / API

URL Ingreso: http://AzureKeyVaultApp

Para el caso de la URL de ingreso puede ser cualquier cosa mientras sea una URL valida.

Damos click en Aceptar/Guardar y se creara la aplicación. Ahora debemos de seleccionar la aplicación que acabamos de crear y recuperar el ID (Application ID), y adicional crear una llave sobre la aplicación, que sera nuestro Secret Id. Estos valores los vamos a ocupar mas adelante.

Creamos una llave para el valor de Secret Id:

Bien, ahora debemos ir a nuestro contenedor de llaves de Azure Key Vault y establecer la aplicación que acabamos de crear como identidad de acceso. Esto lo hacemos dentro de la sección de políticas de acceso. Previamente ya debemos tener nuestro contenedor/almacén de llaves, aquí las instrucciones para hacerlo.

En la sección de politicas de acceso creamos una nueva política de acceso y como identidad buscamos la aplicación que creamos con el nombre de “AzureKeyVaultApp“, y para este caso seleccionamos todos lo permisos.

Una vez hecho esto ya podemos invocar el API desde nuestra aplicación.

En el código de arriba vemos como con los valores de la aplicación que creamos previamente inicializamos 3 variables keyVaultUrlclientId y secret. Después con el método CreateKeyVaultWithSecret creamos nuestro cliente que expone los métodos del API. Si ocupáramos un KeyVaultKeyResolverCachingKeyResolver podemos hacerlo con la instancia de KeyVaultClient.

Registrando Certificado

Primero debemos crear un certificado de prueba. Podemos hacer esto ejecutando los siguientes comandos en la consola de Visual Studio.

En resumen lo que hace las lineas que ejecutamos es crear un certificado con su respectiva llave privada.

Después debemos asociar el certificado a una aplicación de Azure AD. Esto no se puede hacer desde el portal, pero podemos ejecutar el siguiente script sobre Power Shell:

Una vez que se ejecuta exitosamente el script anterior podemos validar en el portal que la aplicación se haya creado, debe tener el nombre de “AzureKeyVaultCert”:

De ahí solo debemos de recupera el Application Id y asociar la aplicación como identidad de acceso a nuestro contenedor de Azure Key Vault, como ya lo hicimos con la aplicación anterior.

Ahora podemos probar esa forma de autenticación en nuestro código. Para hacer pruebas locales debemos instalar el certificado pues lo buscaremos por su thumbprint.

A diferencia del método anterior aquí definimos una variable que contiene el thumbprint del certificado con el que los buscaremos en el método FindCertificateByThumbprint. Lo demás en el código es igual.

Si queremos probar el código locamente debemos instalar el certificado. Si vamos a hacer la instalación sobre un AppService o CloudService debemos subir el certificado que generamos por medio del portal en la sección de “certificados”.

 

Analizador Fonético con Azure Search

Azure Search permite definir analizadores (análisis léxico sobre los términos de consulta) sobre las propiedades que vamos a indexar, existen muchas variedades (algoritmos) de analizadores, existe un analizador fonético que como su nombre lo dice busca coincidencias basadas en fonemas. Ejemplo: zapato y “sapato” coinciden fonéticamente.

Vamos a crear un indice basado en este tipo de analizadores con el SDK de Azure Search (.NET Core C#).

Azure Search

Este servicio de búsqueda que se ofrece dentro de Azure PaaS permite realizar indices (colecciones) de documentos (entidades/ objetos) para realizar búsquedas aplicando full-text search sobre las propiedades de nuestros documentos. La forma de tokenizar el contenido de las propiedades varia según el analizador. Es una buena alternativa para realizar búsquedas cuando se ocupa SQL Azure o algún otro motor de persistencia que no tiene búsquedas basadas en full-text search.

Internamente se “asume” que ocupa ElasticSearch y Lucene.

Los pasos para crear el servicio desde el portal de Azure aquí.

Índice

La definición de nuestro indice se basa en la definición de nuestra clase que vamos a indexar (o agregar a la colección) por medio de atributos indicamos con que criterios se debe generar el índice.

Nuestro índice se refiere a un catalogo de productos con 4 propiedades: id, name, description y country. La clase que lo define se ve así:

Los atributos que vemos sobre las propiedades establecen la definición del índice, que en principio debe coincidir con las entidades que vamos a indexar. Veamos a que se refiere cada uno de los atributos.

IsSearchable: Este atributo indica que la propiedad debe ser tratada para realizar búsquedas sobre full-texte search. Esto implica que el valor que contenga la propiedad se va a tokenizar aplicando un analizador. Entonces si la propiedad tuviera el valor “Blog de Azure” internamente lo tokeniza (separa) formando “Blog” y “Azure”. Esto permite aplicar mejores condiciones de búsqueda.

IsFilterable: Este atributo establece que la propiedad que lo implementa no se realizaran búsquedas por medio de full-text search. Es decir, sobre el ejemplo anterior, si yo busco con la condición “Blog” == “Blog de Azure” no va haber coincidencia, las búsquedas deben ser por frases completas/exactas.

IsSortable: El resultado de cada búsqueda queda ordenado por un valor que va de mayor a menor coincidencia (se genera por medio de TF/IDF), llamado score. Este atributo indica si la propiedad debe afectar el score de los resultados.

Analyzer: Este atributo nos permite indicar que analizador va aplicar a cada propiedad. Como podemos ver estamos aplicando 2 tipos de analizadores. AnalyzerName.AsString.EnLucene este analizador ya esta predefinido y se refiere a que va aplicar un analizador léxico de palabras en ingles de Lucene“PhoneticCustomnAnalyzer” se refiere al analizador fonético que vamos a aplicar, este no es un analizador ya predefinido, este es un analizador customizado que se declara en otro momento.

Key: Solo para definir cual es la llave única para nuestro indice (tiene que ser string), esto es importante pues se ocupa para hacer actualizaciones o eliminar un documento en la colección.

Analizador Fonético

Ahora veamos como crear nuestro analizador personalizado. Como ya habíamos comentado antes, un analizador internamente aplica distintos algoritmos al momento de tokenizar el texto, lo que debemos hacer en nuestro propio analizador es agregar un tipo de tokenizador (tokenizer) y filtros. Aquí la lista.

Un analizador léxico esta definido por:

A tokenizer: divide el texto de entrada en tokens, esto es separar todas la palabras de una frase y remueve palabras que no afectan las búsquedas (stopwords).

A token filters: al momento de generar los tokens se aplican filtros por ejemplo convertir todo los caracteres a lowercase.

Podemos definir cualquiera que se nos ocurra de acuerdo a los resultados que queremos lograr. Definimos un nombre en la propiedad Name y es el que podemos utilizar sobre el atributo Analyzer en la definición de nuestro indice como ya lo vimos.

Creando el Índice

Ocupando los objetos que expone la librería de Azure Search (nuget: Microsoft.Azure.Search) creamos el indice con todo lo que hicimos previamente.

Primero con el método CreateIndexDefinition creamos la definición de nuestro índice, esto incluye obtener la definición de nuestro analizador fonético. Debemos indicar el nombre del índice en la propiedad Name, que lo obtenemos desde la configuración del proyecto, debemos indicar las propiedades del índice con sus atributos en Fields y finalmente indicamos que debe crear un analizador en la propiedad Analyzers.

El método CreateIndexAndGetClient invoca los métodos necesarios para crear el índice. Requerimos primero el nombre del servicio y una key que obtenemos desde el portal de Azure. Validamos si existe previamente el índice, si no, lo creamos.

Una vez que se crea el índice ya no es posible modificar su definición. Para hacer eso hay que crear uno nuevo (o eliminar) y volver a cargar los datos.

Este método también genera el objeto cliente con el que realizaremos las operaciones de búsqueda.

Datos

Necesitamos datos para poder probar nuestro índice, tenemos un archivo json donde tenemos 1,000 objetos que corresponden a la misma estructura del objeto que define nuestro índice.

Estos datos son los que vamos a insertar de forma masiva o en batch. Esto lo permite también el API de Azure Search.

Lo que hacemos con el método CreateBatchData es crear grupos de documentos/entidades para insertar, esto es porque el tamaño máximo de documentos por batch es de 1,000. Aquí el detalle.

Después en el método UploadData creamos la carga de estos grupos en paralelo.

Aquí esta el repositorio del código para ver mas detalle.

Realizando Búsquedas

Aplicar un analizador fonético para nuestras búsquedas hace sentido cuando queremos encontrar resultados aun con faltas de ortografía o probablemente con omisión de letras. También existen mas alternativas para lograr el mismo resultado como ocupar el operador Fuzzy de Lucene o establecer sinónimos.

Dentro de los datos que cargamos tenemos dos propiedades que contienen el mismo valor Name y NamePhonetic, a la primera establecimos un simple analizador léxico natural del lenguaje y en la otra un analizador fonético. El valor que estamos agregando a estas propiedades son marcas de automóviles (Kia, Honda, Mitsubishi, Dodge, Mercedes-Benz, etc).

Vamos a realizar búsquedas y veremos como hace diferencia los analizadores que colocamos.

El método con el que realizamos las búsquedas es SearchPhrase, internamente tiene algunas condiciones para poder ver las diferencias de resultados. PrintResult solo imprime el resultado.

Hagamos el primer ejercicio con la búsqueda de “kia” y “qia” sobre la columna Name.

En el código de arriba invocamos el método SearchPhrase una vez con la frase “kia” y después con la frase “qia” para comparar los resultados. En los datos no existe ningún registro con el valor “qia”. Estos son los resultados:

Sin analizador fonético

Podemos ver que para la frase “qia” no encontró ningún resultado. Ahora hagamos la prueba apuntando hacia la propiedad que tiene el analizador fonético NamePhonetic.

Con analizador fonético

Podemos ver que ahora la frase “qia” si obtuvo el resultados. Esto es porque las frases “kia” y “qia” son iguales fonéticamente.

Los analizadores léxicos de lenguaje si dependen del idioma, es decir cambia entre ingles y español. Para el analizador fonético no en necesario indicar el idioma en el que esta el texto pues Azure Search utiliza un analizador “genérico”.

Como ya lo había dicho antes, esto se podría resolver también con el operador Fuzzy con Lucene. Este operador aplica un algoritmo de aproximación sobre las frases (basado en Damerau-Levenshtein Distance), esto se logra agregando “~” a la frase. Aquí el ejemplo.

Con Fuzzy

Esta vez se hizo con la frase “Jeep” y “Geep” sobre el propiedad Name, y podemos ver que se encontraron los mismos resultados, sin embargo, sobre la búsqueda de “Geep~” encontró un ultimo resultado que fue “Geo” con un score menor, que hace sentido.

Podemos beneficiarnos de ambos analizadores:

 

El código completo de lo que se hace lo pueden ver aqui.

Troubleshooting

Por alguna razón en .NET Core da problemas la versión de 11.* de Newtonsoft.Json al momento de indexar documentos. Hay que agregar la versión 10.* de Newtonsoft.Json y agregar también Microsoft.Rest.ClientRuntime.Azure.

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.