Hablemos sobre .Net y Azure

Autor: Facundo La Rocca (Página 1 de 2)

Startupeando en .Net (Latino.NET OnLine)

Esta semana tuve el placer de volver a dar la charla “Startupeando en .Net 5” para la comunidad de El Camino Dev, pero esta vez para la comunidad de Latino.Net OnLine!

Algunos de los temas que mencioné fueron Múltiples StartupsIStartupFilter y IHostedServices. Además vimos como ejecutar tareas desde el mismo Program.cs utilizando el IServiceProvider que configuramos para la aplicación.

Startupeando en .Net – Latino.NET OnLine

La pesadilla del HttpContext

Hola!!! En este post quiero hablar un poquito sobre uno de los componentes principales de MVC, particularmente de los controladores: el HttpContext.

Todos los desarroladores tarde o temprano nos encontramos con la necesidad de manipular el HttpContext, ya sea para validar si el usuario esta autenticado o para obtener algún claim. Analicemos el siguiente ejemplo:

[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    [HttpGet]
    public IActionResult Greeting()
    {
        if(HttpContext.User.Identity.IsAuthenticated == false)
        {
            return Ok("Hello there!!!");
        }

        var username = HttpContext.User.FindFirst("username")?.Value;
        return Ok($"Hello {username}!!!");
    }
}

Dentro del Controlador tenemos disponible el HttpContext al cuál podemos acceder directamente sin ningún tipo de problemas.

Sin embargo, sabemos que lo aconsejable es utilizar una capa de servicios para desacoplar el Controlador de la capa de negocio. Es cuando introducimos esta capa de servicios donde comienza “La pesadilla del HttpContext”.

Con este ejemplo se verá mucho más claro:

[ApiController]
[Route("[controller]")]
public class UsersController : ControllerBase
{
    private readonly GreetingService _greetingService;

    public UsersController(GreetingService greetingService) =>
        _greetingService = greetingService;

    [HttpGet]
    public IActionResult Greeting()
    {
        var greeting = _greetingService.Greeting(HttpContext);
        return Ok(greeting);
    }
}

public class GreetingService
{
    public string Greeting(HttpContext httpContext)
    {
        if (httpContext.User.Identity.IsAuthenticated == false)
        {
            return "Hello there!!!";
        }

        var username = httpContext.User.FindFirst("username")?.Value;
        return $"Hello {username}!!!";
    }
}

¿Es más claro el problema ahora? Al introducir la capa de servicios, nos vemos obligados a pasar como parámetro el HttpContext. Si tuvieramos otro servicio, entonces tendríamos una cadena de métodos a los cuáles deberíamos pasar el contexto como parámetro. Algo así:

public class GreetingService
{
    public string Greeting(HttpContext httpContext)
    {
        var isAuthenticated = authService.Validate(httpContext);
        if (isAuthenticated == false)
        {
            return "Hello there!!!";
        }

        var username = httpContext.User.FindFirst("username")?.Value;
        return $"Hello {username}!!!";
    }
}

Claro que podríamos pasar como parámetro sólo las variables isAuthenticated y username, pero eso no resolvería el problema de fondo.

¿Y si usaramos inyección de dependencias?

Pienso, todos los servicios pueden ser inyectados si utilizamos el contenedor IoC y sabemos que por cada request se genera un scope (de ahí viene services.AddScoped<>()).

Además sabemos que para cada request se genera un HttpContext al cuál podemos acceder desde el Controlador, desde los filtros de MVC y también lo tenemos disponible en los middlewares.

Entonces, ¿No podríamos recibirlo mediante inyección de dependencias como cualquier otro servicio?

La respuesta es no, no podemos, pero casi…

IHttpContextAccessor

Para resolver el problema de “La pesadilla del HttpContext” tenemos disponible la interfaz IHttpContextAccessor y una implementación default HttpContextAccessor.

Esto nos va a permitir acceder al HttpContext de forma fácil y sencilla sin tener que escribir código de más. Algo así:

public class GreetingService
{
    private readonly IHttpContextAccessor contextAccessor;

    public GreetingService(IHttpContextAccessor contextAccessor) =>
        this.contextAccessor = contextAccessor;

    public string Greeting()
    {
        var httpContext = contextAccessor.HttpContext;
        if (httpContext.User.Identity.IsAuthenticated == false)
        {
            return "Hello there!!!";
        }

        var username = httpContext.User.FindFirst("username")?.Value;
        return $"Hello {username}!!!";
    }
}

Super importante, como con cualquier otro servicio, necesitamos registrarlo apropiadamente. Claro, el framework es nuestro mejor amigo, y siempre nos extiende una mano:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // More services...
        services.AddHttpContextAccessor();
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        // ...
    }
}

Conclusión

Con esta pequeña refactorización evitamos tener que pasar por parámetro el HttpContext, potencialmente en cadena, y así generar código mucho más limpio y entendible.

Startupeando en .Net (El Camino Dev)

Hace algunos días tuve el honor y agrado de participar como speaker para el canal de YouTube de El Camino Dev donde estuve hablando sobre un tema muy interesante pero que no es muy habitual: alternativas disponibles para ejecutar tareas como parte del inicio de una aplicación MVC.

Algunos de los temas que mencioné fueron Múltiples Startups, IStartupFilter y IHostedServices. Además vimos como ejecutar tareas desde el mismo Program.cs utilizando el IServiceProvider que configuramos para la aplicación.

Startupeando – El Camino Dev

Inyección de servicios por ambiente en ASP.Net

Hola de nuevo! Aquí estamos, intentando descubrir algunas funcionalidades “ocultas” de .Net. Hace unos días escribí un artículo sobre como utilizar múltiples Startups en tus aplicaciones MVC.

Siguiendo en la misma línea, hoy veremos otra forma de ordenar la configuración de servicios y del pipeline, esta vez sin la necesidad de tener clases Startup.cs separadas.

Situación actual

Como es habitual, realizamos la configuración de todos los servicios y la configuración del pipeline de nuestra aplicación en la clase Startup.cs. Aquí vemos un ejemplo simple pero muy frecuente:

public class Startup
{
    private readonly IWebHostEnvironment env;

    public Startup(IWebHostEnvironment env) =>
        this.env = env;

    public void ConfigureServices(IServiceCollection services)
    {
        if(env.IsDevelopment())
            services.AddDistributedMemoryCache();
        else
            services.AddDistributedRedisCache(options => { });

        // More services here...
    }

    public void Configure(IApplicationBuilder app)
    {
        if (env.IsDevelopment()) 
            app.UseDeveloperExceptionPage();

        // UseRouting(), UseAuthorization(), ...
    }
}

En el caso de que el ambiente sea Development, configuramos un caché distribuido en memoria, caso contrario utilizamos Redis. De la misma forma, sólo mostramos la página de excepciones si el ambiente es Development.

Requerimiento

Queremos hacer nuestro Startup.cs mas limpios y declarativos, eliminando así la necesidad de tener que validar si el ambiente es Development o no. Idealmente, queremos apoyarnos en el framework sin tener que escribir nuestra propia implementación.

Otra funcionalidad oculta

Sabemos que nuestra clase Startup.cs debe contener dos métodos, ConfigureServices para registrar los servicios necesarias y Configure para configurar el pipeline. Estos métodos son llamados por el runtime en tiempo de ejecución en el momento adecuado.

Sin embargo una funcionalidad no muy conocida es que podemos tener distintos métodos con el nombre Configure{Ambiente}Services y Configure{Ambiente}. Luego el runtime ejecutará los que correspondan al ambiente actual.

Si no existe un método que corresponda al ambiente actual, ejecutará los métodos por defecto ConfigureServices y Configure.

Implementación

El cambio es mu simple, crearemos dos nuevos métodos, ConfigureDevelopmentServices y ConfigureDevelopment, y configuraremos todos los servicios correspondientes al ambiente de desarrollo específicamente:

public class Startup
{
    // Default
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddDistributedRedisCache(options => { });

        // More services...
    }

    // Development
    public void ConfigureDevelopmentServices(IServiceCollection services)
    {
        services.AddDistributedMemoryCache();

        // More services...
    }

    //Default
    public void Configure(IApplicationBuilder app)
    {
        // UseRouting(), UseAuthorization(), ...
    }

    // Development
    public void ConfigureDevelopment(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();

        // UseRouting(), UseAuthorization(), ...
    }
}

Para probarlo, solo debemos establecer algunos breakpoints y ejecutar la aplicación en el ambiente de desarrollo. El framework hará el resto!

Un poco de refactorización

Podemos tener una solución mucho más elegante si creamos un método ConfigureBaseServices conde pongamos todos los servicios comunes, de forma que no tengamos que repetir código (algo que odiamos!):

// Default
public void ConfigureServices(IServiceCollection services)
{
    services.AddDistributedRedisCache(options => { });
    ConfigureBaseServices(services);
}

// Development
public void ConfigureDevelopmentServices(IServiceCollection services)
{
    services.AddDistributedMemoryCache();
    ConfigureBaseServices(services);
}

// Base
private void ConfigureBaseServices(IServiceCollection services)
{
    // Common services...
}

Conclusión

En este post vimos cómo aplicando una pequeña refactorización en la clase Startup.cs podemos sacar provecho de una funcionalidad del framework poco conocida que nos permite configurar los servicios y el pipeline según el ambiente en el que se ejecute la aplicación.

Startups múltiples en .Net 5

Hola! Aquí estamos de nuevo escribiendo sobre .Net. En esta oportunidad vamos a hablar de una funcionalidad que existe desde .Net Core 2.1, pero que sin embargo no es para nada conocida: los Startups múltiples.

Veamos el siguiente código. Corresponde a un Startup.cs de una aplicación Web API muy pequeña, que registra y/o configura servicios en función del ambiente.

public class Startup
{
    private readonly IWebHostEnvironment env;

    public Startup(IWebHostEnvironment env) =>
        this.env = env;

    public void ConfigureServices(
        IServiceCollection services)
    {
        if(env.IsDevelopment())
            services.AddDistributedMemoryCache();
        else
            services.AddDistributedRedisCache(options => { });
        
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        if (env.IsDevelopment())
            app.UseDeveloperExceptionPage();

        // UseRouting, UseAuthorization, etc.
    }
}
  1. Si el ambiente es desarrollo, se configura un cache en memoria y se habilita la página de error.
  2. Si el ambiente NO es desarrollo, se utiliza como cache Redis y no se habilita la página de error.

Funcionar, funciona… Pero ese if validando el ambiente hace ruido. Además de odiar los ifs, mientras más crece la aplicación más crecen esas validaciones, y menos me gusta el código.

La funcionalidad oculta

Como mencionamos al comienzo del post, ya desde la versión 2.1 de .Net Core tenemos disponible una funcionalidad muy poco conocida llamada Startups Múltiples.

¿En qué consiste?

Si creamos una clase Startup con el nombre del ambiente como sufijo y lo registramos, el framework automáticamente ejecutará el Startup.cs que corresponda al ambiente.

Por ejemplo, podemos crear dos Startups, Startup.cs y StartupDevelopment.cs:

public class Startup
{
    public void ConfigureServices(
        IServiceCollection services)
    {
        services.AddDistributedRedisCache(options => { });
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        // UseRouting, UseAuthorization, etc.
    }
}
public class StartupDevelopment
{
    public void ConfigureServices(
        IServiceCollection services)
    {
        services.AddDistributedMemoryCache();
        // More services...
    }

    public void Configure(IApplicationBuilder app)
    {
        app.UseDeveloperExceptionPage();
        // UseRouting, UseAuthorization, etc.
    }
}

Finalmente, en la clase Program.cs dentro del método CreateHostBuilder sólo tenemos que registrar el emsamblado que contiene nuestros Startups.cs y sentarnos a disfrutar de las bondades de .Net.

public static IHostBuilder CreateHostBuilder(string[] args)
{
    var assemblyName = typeof(Startup).Assembly.FullName;

    return Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup(assemblyName);
        });
}

En tiempo de ejecución se seleccionará el Startup cuyo sufijo corresponda con el nombre del ambiente de ejecución. Por ejemplo, si el ambiente es Development, se ejecutará la clase StartupDevelopment. Si no existe el startup correspondiente, se ejecutará la clase por defecto Startup.

¿Es esto siempre necesario?

Bueno, el ejemplo anterior es trivialmente simple como para justificar esta funcionalidad. Esta idea está pensada para aquellos casos donde el startup tiene muchos componentes que dependen del ambiente.

Sobre todo en desarrollo, donde es frecuente que mostremos errores con su stack, cambiemos los niveles de logging, deshabilitemos caches o los usemos en memoria, etc.

Conclusión

Acabamos de explorar una funcionalidad muy poco conocida dentro de .Net llamada Startups Multiples que nos permite tener distintas clases Startup.cs según el ambiente de ejecución. Esto nos permite tener clases más compactas y eliminar la necesidad de tener que validar constantemente el ambiente para inyectar o configurar servicios.

Gestión de configuraciones en Azure Functions

La semana pasada escribí un artículo sobre como inyectar dependencias en Azure Functions, debido a que la plantilla que utilizamos cuando creamos un proyecto nuevo no nos provee los mecanismos necesarios.

Ahora veremos como manejar configuraciones, de forma tal que podamos utilizar el archivo local.settingns.json, variables de entorno y/o User Secrets.

Antes de comenzar

El framework de Azure Functions está preparado para leer las configuraciones que incluimos mediante el portal de Azure en la sección de Configuraciones.

Tomaremos el el concepto de Startup del artículo sobre inyección de dependencias que mencioné anteriormente. Además tomaremos el código base y construiremos sobre él.

Contexto

Si bien cuando desplegamos nuestro código al servicio de Azure Functions las configuraciones configuradas desde el portal se leen automáticamente, puede que necesitemos personalizar el mecanismo con el cual esas configuraciones con leídas.

Por ejemplo, en nuestro ambiente local, usar User Secrets es recomendado. En ambientes pre productivos, podríamos necesitar leer un archivo adicional, como puede ser stage.settings.json.

Requerimiento

El requerimiento de hoy es bastante simple, necesitamos poder leer configuraciones utilizando User Secrets, el archivo local.settings.json y variables de entorno.

Implementación

Cuando creamos el Startup y heredamos de la clase FunctionsStartup, tendremos disponible un método virtual llamado ConfigureAppConfiguration. Es este método es que nos permitirá establecer como son leídas las configuraciones.

public class Startup : FunctionsStartup
{
    public override void Configure(IFunctionsHostBuilder builder)
    {
    }

    public override void ConfigureAppConfiguration(IFunctionsConfigurationBuilder builder)
    {
        var context = builder.GetContext();

        builder.ConfigurationBuilder
            .SetBasePath(context.ApplicationRootPath)
            .AddJsonFile("local.settings.json", optional: true, reloadOnChange: true)
            .AddUserSecrets(Assembly.GetExecutingAssembly(), optional: true)
            .AddEnvironmentVariables();
    }
}

SetBasePath

Siempre que sea necesario leer archivos locales, necesitamos establecer el directorio base. En aplicaciones web o APIs solemos utilizar el directorio actual, .SetBasePath(Directory.GetCurrentDirectory()).

En el caso de Azure Functions necesitamos utilizar el contexto de ejecución, builder.GetContext().ApplicationRootPath.

AddUserSecrets

Cuando instalamos la librería Microsoft.Extensions.Configuration.UserSecrets tenemos que instalar la versión 3.1.XX. Por defecto, el Gestor de Paquetes (Package Manager) instalará la ultima versión, que a la fecha actual es 5.0.0, sin embargo el framework todavía no fue actualizado y sigue utilizando la versión de .Net Core 3.1.

Es inminente la actualización del framework de Azure Functions a la version .Net 5, pero al momento de escribir este artículo, todavía utiliza la versión 3.1.

Conclusión

En este breve post vimos cómo podemos configurar nuestra Azure Function para utilizar los distintos tipos de mecanismos para manejo de configuraciones utilizando todas las herramientas que le framework nos provee.

Con muy poco código, tenemos disponible variables de entorno, User Secrets y archivos json.

Inyección de dependencias en Azure Functions

A estas alturas, inyección de dependencias es uno de esos patrones de diseño que se ha convertido casi obligatorio, no sólo porque nos ayuda a escribir código mas testeable, sino también a escribir componentes desacoplados. No importa que tan compleja o simple sea una clase, si tiene alguna dependencia la inyectaremos mediante el constructor.

En este post veremos los pasos necesarios para poder comenzar a utilizar inyección de dependencias de la misma forma que lo hacemos en cualquier aplicación web.

Contexto

Cuando iniciamos un proyecto nuevo de Azure Functions, a diferencia de como sucede con proyectos de ASP.Net, no disponemos de ningún mecanismo listo para usar para inyectar dependencias.

A modo de ejemplo, pensemos una Azure Function llamada “Greeting” del tipo HttpTrigger que en base a un nombre devuelve un saludo.

Como parte de nuestra solución, implementamos un servicio muy simple de forma que nuestro código sea testeable.

public class GreetingService : IGreetingService
{
    public string GetGreeting(string name) =>
        string.IsNullOrEmpty(name)
            ? "Hi there!"
            : $"Hi {name}!";
}

Luego, implementamos el código de la función de forma tal que lea el nombre recibido como query string y obtenga el saludo correspondiente utilizando el servicio anterior.

public static class Greeting
{
    [FunctionName("Greeting")]
    public static IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
    {
        var name = req.Query["name"];
        var greetingService = new GreetingService();

        var greeting = greetingService.GetGreeting(name);

        return new OkObjectResult(greeting);
    }
}

En la línea 8 podemos apreciar el problema: para poder utilizar el servicio necesitamos crear una instancia del mismo.

Requerimiento

Necesitamos implementar Inyección de Dependencias, no sólo porque las buenas prácticas así lo recomiendan, sino también porque queremos escribir Test Unitarios, y sabemos que de la forma que está escrito nuestro código es imposible.

Parte del requerimiento consiste en utilizar todas las herramientas que le framework nos provea, de forma que no tengamos que implementar nuestro propio set de clases o Contenedor para inyección de dependencias.

Implementación

Para poder completar nuestro requerimiento necesitamos dos cosas:

  1. Registrar y configurar nuestros servicios (generalmente cuando se inicia la aplicación)
  2. Inyectar las dependencias en forma automática, como lo hacemos en aplicaciones Web o APIs con ASP.Net.

Por suerte, el framework de Azure Functions nos provee de ambos mecanismos, solo tenemos que realizar las configuraciones necesarias.

Librerías

Comenzaremos instalando la librería Microsoft.Azure.Functions.Extensions, la cuál nos va facilitar los componentes que necesitamos.

dotnet add package Microsoft.Azure.Functions.Extensions

Startup

Azure Functions dispone de una clase abstracta diseñada para ser ejecutada al iniciar la aplicación. Esta clase se llama FunctionsStartup y nos va a permitir realizar la registración de servicios que necesitamos.

Comenzaremos agregando una nueva clase al proyecto llamada Startup y haremos que herede de la clase FunctionsStartup.

using Demo;
using Microsoft.Azure.Functions.Extensions.DependencyInjection;

[assembly: FunctionsStartup(typeof(Startup))]

namespace Demo
{
    public class Startup : FunctionsStartup
    {
        public override void Configure(IFunctionsHostBuilder builder)
        {
        }
    }
}

El atributo de ensamblado (línea 4) es fundamental para que el framework pueda detectar los startups que deben ser invocados.

Configure

Al heredar de la clase abstracta FunctionsStartup, tendremos que sobrescribir el método Configure. Aquí haremos la registración de los servicios necesarios, igual que lo hacemos en cualquier aplicación ASP.Net. Siguiendo con el ejemplo, registraremos la clase GreetingService.

public override void Configure(IFunctionsHostBuilder builder)
{
    builder.Services.AddSingleton<IGreetingService, GreetingService>();
}

Function

El último paso que nos queda es modificar la función para poder inyectar los servicios necesarios como lo haríamos con cualquier otra clase. Las modificaciones son:

  1. Modificar la clase y el método Run para que ya no sean static.
  2. Agregar un constructor parametrizado con los servicios que queremos recibir.
  3. Guardar los servicios en variables de instancia.
  4. Modificar el metodo en cuestion para utilizar las variables de instancia
public class Greeting
{
    private readonly IGreetingService _greetingService;

    public Greeting(IGreetingService greetingService) =>
        _greetingService = greetingService;

    [FunctionName("Greeting")]
    public IActionResult Run(
        [HttpTrigger(AuthorizationLevel.Anonymous, "get")] HttpRequest req)
    {
        var name = req.Query["name"];
        var greeting = _greetingService.GetGreeting(name);

        return new OkObjectResult(greeting);
    }
}

Conclusión

En este breve post vimos cómo podemos configurar nuestra Azure Function para utilizar Inyección de Dependencias utilizando todas las herramientas que le framework nos provee.

Si querés acceder al código completo, te dejo a continuación el repositorio en Github.

User Secrets: protegiendo configuraciones sensibles .Net 5

¿Estás usando appsetings.json o appsettings.development.json para guardar información sensible? ¿Has “commiteado” alguna vez cadenas de conexión, secretos privados o llaves de aplicación por accidente?

Si la respuesta a las preguntas anteriores es afirmativa y nunca habías escuchado hablar de User Secrets, entonces este artículo te puede interesar.

Contexto

En la mayoría de nuestras aplicaciones necesitamos interactuar con algún tipo de API, servicio o base de datos que requiere la utilización de credenciales ya sea en forma de cadena de conexión (connection string), llaves de API (API Keys) o secretos (secrets).

Cómo programadores, no nos corresponde ocuparnos de la gestión de esas credenciales en ambientes productivos, pero sí nos corresponde manejarlas y protegerlas correctamente en nuestros ambientes locales.

Si bien hay diferentes formas de manejar credenciales (variables de entorno para producción por ejemplo), es muy frecuente la utilización de archivos appsettings.json y/o appsettings.development.json.

Problema

Para ejecutar nuestra aplicación en forma local, solemos utilizar el archivo appsettings.development.json para colocar aquellas configuraciones que dependen del ambiente, como por ejemplo las cadenas de conexión a base de datos. Por ejemplo:

{
  "ConnectionStrings": {
    "Database": "Server=dev.server;Database=Products;User Id=dev;Password=Passw0rd;"
  },
  "PricesEndpoint": "https://dev.server.com/api/prices?key=xkihf57845addg"
}

El problema con este método es que al momento de subir nuestros cambios al repositorio es muy fácil olvidarnos de este archivo e incluirlo como parte de la implementación.

.gitignore como alternativa

Un posible solución al problema anterior es incluir el archivos appsettings.development.json en el archivo .gitignore de forma que git ignore los cambios. En escenarios muy simples esto puede funcionar sin problemas.

Sin embargo, es común que haya ciertas configuraciones de facto que se van agregando o modificando con el tiempo, y que luego cada programador puede ir modificando en forma local según sea el caso. Por ejemplo:

{
  // Default for development environment.
  "UseCache":  false
}

Esto puede resultar bastante tedioso y confuso de manejar si el archivo appsettings.development.json se encuentra ignorado.

Solución: User Secrets

El Secret Manager es una funcionalidad desarrollada específicamente para manejar información sensible en ambientes locales de desarrollo de una forma más segura, y se ocupa de la gestión de los User Secrets.

Este guarda las piezas de información sensible dentro de un archivo que se encuentra en un directorio totalmente diferente al directorio del proyecto, de forma que nuestro sistema de control de cambios no lo vea como parte de las modificaciones.

¡Jamás volverás a “commitear” cadenas de conexión o secretos!

Activar User Secrets

Secret Manager funciona por proyecto, es decir, lo tenemos que activar en todos los proyectos donde lo necesitamos.

Podemos hacerlo mediante la línea de comandos utilizando .NET Cli ejecutando el siguiente comando en la carpeta del proyecto:

dotnet user-secrets init

O también podemos hacerlos desde Visual Studio simplemente haciendo clic derecho sobre el proyecto y luego seleccionando la opción Administrar secretos de usuario:

Administrar secretos de usuario desde Visual Studio
Administrar secretos de usuario desde Visual Studio

Al activar los secretos de usuario, se genera una entrada en el archivo del proyecto (.csproj) llamada UserSecretsId con un guid:

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>net5.0</TargetFramework>
    <UserSecretsId>0dc8fc18-564f-4c6e-9602-65fae16ab231</UserSecretsId>
  </PropertyGroup>
</Project>

Ese guid corresponde al nombre de un directorio creado %APPDATA%\Microsoft\UserSecrets\ (Windows) o ~/.microsoft/usersecrets/ (Linux / MacOS), dentro del directorio encontraremos un archivo en formato JSON llamado secrets.json con las configuraciones que hayamos agregado.

Directorio de secretos de usuario
Directorio de secretos de usuario

Agregar User Secrets

Podemos agregar secretos de usuario mediante el cliente .NET Cli utilizando el siguiente comando:

dotnet user-secrets set "ConnectionStrings:Database" "Server=..."

dotnet user-secrets set "PricesEndpoint" "https://dev..."

En el primer comando los “:” (dos puntos) se utilizan para indicar los niveles de objetos de un JSON

También podemos hacerlo desde Visual Studio mediante la opción Administrar secretos de usuario como vimos anteriormente. La ventaja de esta alternativa es que Visual Studio nos permite manejar los secretos directamente como un archivo JSON, exactamente igual al archivo appsettings.json:

{
  "ConnectionStrings": {
    "Database": "Server=..."
  },
  "PricesEndpoint": "https://dev..."
}

Leer User Secrets

Los secretos de usuario se leen como cualquier otra configuración una vez que fueron cargados apropiadamente. Por ejemplo, podemos acceder a la configuración PricesEndpoint mediante la interfaz IConfiguration:

public class Startup
{
    public Startup(IConfiguration configuration) =>
        Configuration = configuration;

    public IConfiguration Configuration { get; }

    public void ConfigureServices(IServiceCollection services)
    {
        var pricesEndpoint = Configuration["PricesEndpoint"];
        // O Configuration.GetConnectionString("Database")        
        var connectionString = Configuration["ConnectionStrings:Database"];
        services.AddControllers();
    }

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
    {
    }
}

Para que esto funcione es necesario cargar primero los secretos de usuario. Si estamos utilizando Host.CreateDefaultBuilder(args) en la clase Program.cs, entonces no es necesario hacer nada mas ya que los secretos de usuario se cargar en forma automática.

public class Program
{
    public static void Main(string[] args) =>
        CreateHostBuilder(args).Build().Run();

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });
}

En el caso de que no estemos utilizando Host.CreateDefaultBuilder(args) o querramos cambiar el comportamiento, tenemos que utilizar el método de extensión AddUserSecrets().

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostContext, builder) =>
        {
            builder.SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
                .AddJsonFile("appsettings.development.json", optional: true, reloadOnChange: true)
                .AddUserSecrets<Program>(optional: true, reloadOnChange: true)
                .AddEnvironmentVariables();
        })
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        });

El orden en el que se incluyen las distintas fuentes de configuración es importante, ya que las ultimas sobrescriben a las primeras.

Debido a que los User Secrets sólo se usan en el ambiente local de cada desarrollador, lo recomendable es incluirlos sólo y sólo si el ambiente es Desarrollo:

Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((hostContext, builder) =>
    {
        var environment = hostContext.HostingEnvironment;
        // ...
        if(environment.IsDevelopment())
        {
            builder.AddUserSecrets<Program>(optional: true, reloadOnChange: true);
        }
        // ...
    })
    // ...

Conclusión

En este artículo aprendimos que incluso en ambientes locales de desarrollo debemos manejar información sensible, como cadenas de conexión y secretos, con el mismo cuidado que lo hacemos en producción.

Además aprendimos cómo User Secrets puede ayudarnos a evitar dolores de cabeza en la gestión de información sensible en nuestros ambientes de desarrollo locales de una forma muy simple y práctica.

Evitar usuarios de redes sociales duplicados en Azure AD B2C (Graph)

En artículo anterior de la serie armamos nuestro Conector y configuramos Azure AD B2C para utilizarlo y así detectar y prevenir usuarios duplicados. Además creamos una Azure Function con código base para probar el conector.

En este post final completaremos la implementación escribiendo el código necesario para consultar Microsoft Graph API por usuarios con el mismo email ya sea como identidad o email adicional.

Preparación

Antes de comenzar con la implementación necesitamos recolectar las siguientes piezas de información y hacer algunas configuraciones:

  1. ID de Tenant o Directorio
  2. ID de Aplicación
  3. Generar un Secreto de Cliente
  4. Dar permisos para Microsoft Graph API

ID de Tenant y ID de Aplicación

Para obtener las primeras dos piezas de información que necesitamos debemos ir al registro de aplicaciones seleccionar la aplicación con la cuál vamos a trabajar. Cuándo realizamos la configuración inicial creamos un registro de aplicación llamado Sitio Web Increíble, ese es el que seleccionaremos en este caso.

Dela sección Información general del registro de aplicación podremos ver la información necesaria.

Registro de aplicación
Registro de aplicación

Generar secreto de cliente

Desde el mismo registro de aplicación podremos generar un secreto de cliente. En la sección Certificados y Secretos creamos un Nuevo secreto de cliente. Sólo necesitamos completar la descripción (la cuál es usada como referencia) y seleccionar una expiración.

Generación de secreto de cliente
Generación de secreto de cliente

Al finalizar veremos el secreto de cliente en la lista de secretos, necesitamos copiar el valor. Es importante tener en cuenta que una vez que abandonemos esta página ya no podremos a volver a ver el valor.

Valor del secreto de cliente
Valor del secreto de cliente

Permisos para Microsoft Graph API

Desde la sección Permisos de API en el registro de aplicación, agregamos un nuevo permiso de Microsoft Graph. Dentro de la categoría Permisos de Aplicación agregamos el permiso User.Read.All como se muestra a continuación:

Permisos de Microsoft Graph para leer usuarios

Es muy importante conceder consentimiento permisos de administrador para nuestro tenant, de lo contrario los permisos no serán efectivos.

Implementación

Si bien Microsoft Graph expone una API REST muy bien documentada, el equipo de Microsoft creó una librería para facilitarnos la tarea y que se encuentra disponible vía paquetes de NuGet. Comenzaremos agregando al proyecto de Azure Function los siguientes paquetes de NuGet:

Microsoft.Graph
Microsoft.Graph.Auth
Microsoft.Identity.Client

Luego, a partir del ID de Aplicación, el ID de Tenant y el Secreto de Cliente que obtuvimos anteriormente, vamos a generar una instancia de GraphServiceClient:

// Usings
using Microsoft.Graph;
using Microsoft.Graph.Auth;
using Microsoft.Identity.Client;

// Código
var confidentialClientApplication = ConfidentialClientApplicationBuilder
    .Create("<< ID de Aplicación >>")
    .WithTenantId("<< ID de Tenant >>")
    .WithClientSecret("<< Secreto de Cliente >>")
    .Build();

var authProvider = new ClientCredentialProvider(confidentialClientApplication);

var graphClient = new GraphServiceClient(authProvider);

Buscando usuarios por email

Estamos buscando encontrar usuarios con el mismo email que el usuario actual está intentando usar, con lo cuál buscamos usuario cuya identidad sea el mail o usuarios que tengan el email como parte de sus emails adicionales. Esto requiere dos peticiones diferentes.

  • Búsqueda por identidad
var usersByIdentity = (await _graphClient.Users
    .Request()
    .Filter($"identities/any(c:c/issuerAssignedId eq '{email}' and c/issuer eq '{"<< ID de Tenant >>"}')")
    .GetAsync())
        .ToArray();
  • Usuarios por otros emails:
var usersByOtherEmails = (await _graphClient.Users
    .Request()
    .Filter($"otherMails/any(c:c eq '{email}') and UserType eq 'Member'")
    .GetAsync())
        .ToArray();

Si alguna de las dos peticiones anteriores contiene algún resultado, eso quiere decir entonces que hemos encontrado otro usuario con el mismo email que el usuario actual y demos detener el flujo de usuario. De lo contrario podemos continuar con la ejecución del flujo de usuario.

Quiero hacer mención al los usuarios de Stackoverflow boehlefeld y LuisEduardox de quién tomé las dos consultas necesarias para interactuar con Microsoft Graph a partir de las respuestas aquí y aquí mencionadas.

Como vimos en el post anterior, detenemos la ejecución del flujo de usuario retornando como respuesta "action" : "ShowBLockPage".

if (usersByIdentity.Any() || usersByOtherEmails.Any())
{
    return new OkObjectResult(
        new
        {
            version = "1.0.0",
            action = "ShowBlockPage",
            userMessage = $"Email {req.email} is duplicated."
        });
}

return new OkObjectResult(
    new
    {
        version = "1.0.0",
        action = "Continue"
    });

Solución completa

Juntando todas las piezas, nuestra Azure Function se ve de la siguiente forma:

var confidentialClientApplication = ConfidentialClientApplicationBuilder
    .Create("<< ID de Aplicación >>")
    .WithTenantId("<< ID de Tenant >>")
    .WithClientSecret("<< Secreto de Cliente >>")
    .Build();

var authProvider = new ClientCredentialProvider(confidentialClientApplication);

var graphClient = new GraphServiceClient(authProvider);

var usersByIdentity = (await _graphClient.Users
    .Request()
    .Filter($"identities/any(c:c/issuerAssignedId eq '{email}' and c/issuer eq '{"<< ID de Tenant >>"}')")
    .GetAsync())
        .ToArray();

var usersByOtherEmails = (await _graphClient.Users
    .Request()
    .Filter($"otherMails/any(c:c eq '{email}') and UserType eq 'Member'")
    .GetAsync())
        .ToArray();

if (usersByIdentity.Any() || usersByOtherEmails.Any())
{
    return new OkObjectResult(
        new
        {
            version = "1.0.0",
            action = "ShowBlockPage",
            userMessage = $"Email {req.email} is duplicated."
        });
}

return new OkObjectResult(
    new
    {
        version = "1.0.0",
        action = "Continue"
    });

Sólo nos resta desplegar la nueva versión de nuestra Azure Function y probar.

Conclusión

En este post nos enfocamos en la parte final de la solución la cual implicó la interacción con Microsoft Graph para detectar usuarios con el mismo email que el usuario que se está intentando crear.

Una solución un poco más elegante pero mucho más compleja es la unificación de identidades, para lo que necesitamos utilizar Políticas Personalizadas de Azure AD B2C. Dejaremos esa para más adelante.

Para ver la implementación final en detalle te recomiendo revisar el repositorio en GitHub.

Controllers y CancellationTokens en .Net 5

Hola! Quiero comenzar el post con la siguiente pregunta:
Si haces una solicitud a tu Web Api desde un cliente REST (Postman por ejemplo) y luego la cancelas antes de que termine, ¿Qué sucede?

En pocas palabras, podría no pasar nada o podríamos estar desperdiciando recursos sin necesidad. Todo depende de cómo estés escribiendo los controladores.

En los próximos párrafos intentaré responder esa pregunta, y mediante ejemplos, veremos como solucionarlo.

Caso de uso

Analicemos el siguiente controlador MVC de una aplicación Web API regular. Es un controlador muy simple que simula una operación costosa. En este caso es un simple Task.Delay() de 5 y 10 segundos respectivamente.

[HttpGet]
public async Task<IActionResult> Get()
{
    _logger.LogInformation("1- Ready to start");

    await Task.Delay(TimeSpan.FromSeconds(5));

    _logger.LogInformation("2- Half of the work done");

    await Task.Delay(TimeSpan.FromSeconds(10));

    _logger.LogInformation("3- We finished!!!");

    return Ok();
}

Y ahora lo probaremos desde Postman. Para esto ejecutamos la solución y luego hacemos una petición a nuestra API. La idea es esperar los primeros 5 segundos y luego cancelar la petición desde Postman. La petición completa toma algo más de 15 segundos.

Cancelación de la solicitud
Cancelación de la solicitud

Observación

En la imagen anterior vemos sobre la izquierda Postman y sobre la derecha la consola de Visual Studio mostrando los logs de la API.

Algo muy interesante es que a pesar de haber cancelado la petición luego de los 5 segundos los logs siguen apareciendo en la consola. Como podemos apreciar, Postman efectivamente cancela la solicitud, pero la API parece ignorar la cancelación.

Si efectivamente estuviéramos realizando una operación costosa, cómo puede ser acceso a base de datos o comunicación con otros servicios, habríamos desperdiciados recursos procesando parte de una solicitud que ya no tiene sentido debido a que el cliente desistió de la misma.

Aspectos técnicos

Esta situación donde una tarea es cancelada o abortada es bastante frecuente en distintos tipos de aplicaciones (ya sean web, desktop o nativas) y el framework nos provee mecanismos para hacer la tarea mucho mas simple.

A su vez, Asp.Net esta preparado para aprovechar esos mecanismos y detectar cuando el cliente canceló la solicitud o la conexión ha sido interrumpida, es nuestra responsabilidad hacer uso de ellos.

CancellationTokens

Cada vez que una acción es ejecutada el framework inyectará una instancia de un CancellationToken enlazada al ciclo de vida de la petición. De esta forma podremos detectar si la la petición ha sido cancelada.

Podemos lanzar un excepción del tipo TaskCancelledException como en el siguiente ejemplo:

[HttpGet]
public IActionResult Get(CancellationToken token)
{
    token.ThrowIfCancellationRequested();

    return Ok();
}

O bien podemos validar si se ha solicitado su cancelación:

[HttpGet]
public IActionResult Get(CancellationToken token)
{
    if(token.IsCancellationRequested)
    {
        return BadRequest("Solicitud cancelada.");
    }

    return Ok();
}

Pasando CancellationTokens en la cadena de ejecución

En general, es común que los métodos que impliquen algún tipo de tarea asincrónica (I/O en general como comunicación con base de datos, manejos de streams, network, etc.) acepten un CancellationToken como último parámetro.

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    await Task.Delay(TimeSpan.FromSeconds(5), token);

    return Ok();
}

Modificando el controlador

Cómo vimos en el ejemplo anterior, el cambio necesario para manejar correctamente la cancelación de la solicitud solo requiere agregar un parámetro adicional CancellationToken a la acción del controlador y pasar ese token al método Task.Delay().

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    _logger.LogInformation("1- Ready to start");

    await Task.Delay(TimeSpan.FromSeconds(5), token);

    _logger.LogInformation("2- Half of the work done");

    await Task.Delay(TimeSpan.FromSeconds(10), token);

    _logger.LogInformation("3- We finished!!!");

    return Ok();
}

Esta vez el comportamiento será diferente:

Cancelación de la solicitud - Error
Cancelación de la solicitud – Error

Dos cosas interesantes podemos destacar:

  1. Por un lado la tarea efectivamente fue cancelada y la ejecución de la petición no continuo.
  2. Se produjo una excepción

TaskCancelledException

Cuando un CancellationToken recibe la notificación de cancelación arroja una excepción del tipo TaskCancelledException. Es nuestra responsabilidad manejar en forma correcta esta situación.

Es importante mencionar que la forma en la que manejamos las cancelaciones es completamente arbitraria y depende del tipo de software y reglas de negocio con las que estemos trabajando.

A fines prácticos para nuestro ejemplo, voy a utilizar un simple try/catch.

[HttpGet]
public async Task<IActionResult> Get(CancellationToken token)
{
    try
    {
        _logger.LogInformation("1- Ready to start");

        await Task.Delay(TimeSpan.FromSeconds(5), token);

        _logger.LogInformation("2- Half of the work done");

        await Task.Delay(TimeSpan.FromSeconds(10), token);

        _logger.LogInformation("3- We finished!!!");

        return Ok();
    }
    catch (TaskCanceledException)
    {
        _logger.LogInformation("Task was cancelled");

        return BadRequest();
    }
}
Cancelación de la solicitud - Éxito
Cancelación de la solicitud – Éxito

Consideraciones

Un aspecto muy importante a tener en cuenta es qué tipo de consecuencias puede generar la cancelación de la solicitud en términos de consistencia, ya que podríamos potencialmente truncar una operación en la mitad.

Por ejemplo, si estamos trabajando con Entity Framework Core es conveniente agrupar las operaciones dentro de una transacción y pasar el CancellationToken. CommitAsync() y RollbackAsync() también aceptan un CancellationToken.

using (var transaction = await _dbContext.Database.BeginTransactionAsync(token))
{
    // Modifying operations here

    // Commit if successful
    await transaction.CommitAsync(token);
    // Rollback otherwise
    await transaction.RollbackAsync(token);
}

Conclusión

Por un lado podemos concluir que es importante contemplar la idea de que la petición puede ser abortada o cancelada, y podríamos estar malgastando recursos innecesarios.

Por el otro lado y para suerte nuestra, el framework nos provee todos los mecanismos necesarios para que la tarea se fácil y sencilla mediante la utilización de CancellationTokens.

Versionando APIs con .NET 5 (Net-Baires)

Uno de los grandes desafíos a la hora de diseñar y desarrollar REST APIs es cómo encarar aquellos cambios que afectan a los clientes, conocidos como breaking changes.

Para evitar los breaking changes es indispensable aplicar una estrategia de versionado clara y efectiva.

En esta sesión cuento todo lo que necesitamos saber para versionar nuestras APIs de forma eficiente y efectiva utilizando todo el poder que .NET 5 nos otorga.

El código completo de los ejemplos, como siempre disponible en mi GitHub.

Evitar usuarios de redes sociales duplicados en Azure AD B2C (Conectores)

En artículo anterior discutimos sobre este posible inconveniente cuando trabajamos con distintos proveedores de identidad y planteamos una propuesta de arquitectura combinando Conectores, Azure Functions y Microsoft Graph API..

En este post vamos a analizar qué son los Conectores y cómo nos pueden ser de gran ayuda para requerimientos de este tipo. Además crearemos un conector junto con una API muy simple utilizando Azure Functions.

Conectores

Ya vimos que los conectores nos proporcionan un mecanismo para ejecutar REST APIs antes de que Azure AD B2C haga la creación del usuario en la base de datos. Y vimos que se pueden ejecutar después de haber iniciado sesión con un proveedor de identidad externo y antes de crear el usuario.

Descubrimos además que sólo se ejecutan durante el registro de nuevos usuarios. Veamos entonces los aspectos técnicos más relevantes para poder completar nuestra implementación.

Casos de uso

Los conectores nos permiten extender la experiencia de registro e integrarla con sistemas externos:

  • Validación de datos: podemos validar determinados datos de entrada como puede ser el formato correcto de de identificación personal o cuenta bancaria.
  • Aprobación: en algunos casos prevenir la creación del usuario si no fue autorizado aún en una plataforma interna.
  • Agregar o sobrescribir atributos: podríamos sobrescribir algunos atributos, ya sea para darle un formato específico (Camel case). También podemos agregar o auto completar atributos.
  • Lógica de negocio: por ejemplo, notificar a otros sistemas externos.

En nuestro caso, validaremos contra Microsoft Graph si existe un usuario con ese email.

Solicitud (Request)

Cuando un conector es ejecutado envía una solicitud HTTP POST a la REST API que hemos configurado oportunamente. El cuerpo de la solicitud será enviado en formato JSON (Content-type: application/json), por ejemplo:

{
 "email": "johnsmith@facutherock.net",
 "identities": [
     {
     "signInType":"federated",
     "issuer":"github.com",
     "issuerAssignedId":"0123456789"
     }
 ],
 "displayName": "John Smith",
 "givenName":"John",
 "lastName":"Smith",
 "extension_<extensions-app-id>_Atributo1": "Atributo personalizo 2",
 "extension_<extensions-app-id>_Atributo2": "Atributo personalizo 2",
 "ui_locales":"es-ES"
}

Los atributos o claims que no tengan valor (null or undefined) no serán informados en el JSON enviado como solicitud.

Tipo de respuesta (Response)

Nuestra REST API debe devolver una respuesta al conector de Azure AD B2C que luego será utilizada para continuar con el flujo de usuario. La respuesta debe estar en formato JSON (Content-type: application/json).

Hay tres tipos de respuesta:

  • Respuesta de continuación:
    Indica que el flujo debe continuar a la siguiente etapa. Si estamos ejecutando un conector luego del inicio de sesión con un proveedor externo, la siguiente etapa será la recolección de atributos adicionales. Si el conector ejecutado es antes de crear el usuario, la etapa siguiente es la creación del usuario.
    El código de estado de la respuesta debe ser 200 - OK y debe contener un JSON con un elemento "action" cuyo valor debe ser "Continue", como el siguiente:
{
    "version": "1.0.0",
    "action": "Continue",
    "postalCode": "12349", // atributo adicional o auto-completado
    "extension_<extensions-app-id>_CustomAttribute": "value" // Atributo personalizado adicional o auto-completado
}
  • Respuesta de bloqueo:
    Detiene el flujo de usuario y muestra un mensaje al usuario. El código de estado de la respuesta debe ser 200 - OK y debe contener un JSON con un elemento "action" cuyo valor debe ser "ShowBlockPage", como el siguiente:
{
    "version": "1.0.0",
    "action": "ShowBlockPage",
    "userMessage": "Hubo un error.",
}
  • Respuesta de validación:
    Esta respuesta sólo es válida en la recolección de atributos adicionales y muestra un error al usuario en relación a la información ingresada. Por ejemplo, "Apellido es obligatorio".
    El código de estado de la respuesta debe ser 400 - Bad Request, debe contener un JSON con un elemento "action" cuyo valor debe ser "ValidationError" y otro elemento "status" cuyo valor debe ser 400 o "400", como el siguiente:
{
    "version": "1.0.0",
    "status": 400,
    "action": "ValidationError",
    "userMessage": "Apellido es obligatorio."
}

Definiciones

Para la implementación que necesitamos, donde sólo queremos evitar usuarios repetidos con el mismo email pero usando diferentes proveedores de identidad, usaremos respuestas de continuación y respuestas de bloqueo.

La lógica responderá de la soguiente forma:

  • Si existe un usuario con el email indicado, devolveremos "ShowBlockPage".
  • Si no existe un usuario con el email indicado, devolveremos "Continue".

Manos a la obra

En este post nos concentraremos sólo en los conectores y dejaremos para el próximo la lógica necesaria para interactuar con Microsoft Graph API.

  1. Crearemos una Azure Function para atender la petición del conector.
  2. Crearemos un conector y lo asociaremos a la Azure Function.
  3. Configuraremos el flujo de usuario para utilizar el conector.

Azure Function

Para no extender demasiado el texto ni desviar el foco, obviaré la creación y publicación de un proyecto de Azure Functions. Si necesitas ayuda con eso, podrás encontrarlo aquí.

Utilizaremos una función del tipo HttpTrigger. Por el momento la lógica de la función será muy simple:

  • Si el dominio del email es "gmail.com" entonces devolvemos "ShowBlockPage" con el mensaje adicional “Usuario duplicado".
  • En cualquier otro caso devolvemos "Continue".
[FunctionName("PreventUserDuplication")]
public static IActionResult Run(
    [HttpTrigger(AuthorizationLevel.Anonymous, "post")] dynamic req)
{
    if (req.email.EndsWith("gmail.com"))
    {
        return new OkObjectResult(
            new
            {
                version = "1.0.0",
                action = "ShowBlockPage",
                userMessage = $"Email {req.email} is duplicated."
            });
    }
    return new OkObjectResult(
        new
        {
            version = "1.0.0",
            action = "Continue"
        });
}

Finalmente debemos publicar el código y obtener la URL correspondiente ya que la necesitaremos en el siguiente paso.

La implementación que estamos realizando es solo “de muestra” y no es la implementación final. La idea es entender como funcionan los conectores.

Crear conector

Desde la sección Conectores de API agregaremos un nuevo conector.

Conectores de API
Conectores de API
Configuración de conector
Configuración de conector

En la pantalla de configuración del nuevo conector elegiremos un nombre para mostrar y luego completaremos el campo Dirección URL del punto de conexión con la URL de la Azure Function del punto anterior.

Tipo de autenticación usaremos Básico y para Nombre de Usuario y Contraseña pondremos cualquier valor , más adelante veremos por qué.

Al finalizar guardamos los cambios y terminamos.

Flujo de usuario

Flujo de usuario - Conectores
Flujo de usuario – Conectores

La configuración del flujo de usuario es bastante simple, solo debemos seleccionar que conector usar y en qué parte del flujo.

Siguiendo con nuestro, vamos a utilizar el mismo conector en ambas instancias, luego del inicio de sesión con un proveedor externo y antes de la ceación del usuario.

Pruebas

Para las pruebas intentaremos registrar un nuevo usuario con una cuenta de email terminada en “gmail.com” y otra terminada con cualquier dominio.

El flujo de usuario debería bloquear al usuario si la cuenta de email es de “gmail.com” y debería continuar con otras cuentas.

Usuario duplicado
Usuario duplicado

Para proveedores de identidad externos el comportamiento será el mismo.

Seguridad

Al momento de escribir este post, los conectores aceptan como mecanismo de seguridad autenticación básica o certificados (preview).

El mecanismo que elegimos anteriormente es Básico, el cuál requiere Usuario y Contraseña.

Es responsabilidad de la API validar correctamente el usuario y contraseña al recibir el request.

La ventaja de utilizar Azure Functions es que podemos utilizar la seguridad integrada que el servicio ofrece mediante llaves de función (Function Keys)

Conclusión

Hemos implementado en forma satisfactoria nuestra primera integración a través de Conectores de API, y si bien la implementación es solo “de muestra”, nos permitió entender como funcionan.

Para poder completar la implementación del requerimiento sólo nos falta completar la implementación de la Azure Function para integrarnos con Microsoft Graph API, te lo cuento en el próximo post!

Unit-Testeando HttpClient con .NET 5

Ya desde las primeras versiones, .NET Core utiliza inyección de dependencias desde sus cimientos, y provee un completo mecanismo de Inversión de Control (IoC) para hacernos la vida más simple.

Podemos registrar cualquier clase y delegar en el framework la tarea de generar instancias e inyectar los objetos, y la clase HttpClient no es la excepción. Ahora, cómo inyectamos HttpClient fakes?

Ahora veremos todo lo necesario para poder escribir buenos test unitarios con excelentes HttpClientfakeados“!

Un fake es un objeto usamos para simular un comportamiento específico. Es uno de los conceptos de las pruebas unitarias.

Contexto

Hemos estado trabajando en un nuevo servicio que realiza una petición HTTP como parte de las reglas de negocio.

public class StockService
{
    private readonly HttpClient _httpClient;

    public StockService(HttpClient httpClient) =>
        _httpClient = httpClient;

    public async Task<string> IsAvailableAsync(bool check)
    {
        if (check == false)
            return "N";

        var result = await _httpClient.GetAsync("stock");

        // To keep it simple, we ignore other results
        return result.StatusCode switch
        {
            HttpStatusCode.OK => "S",
            HttpStatusCode.BadRequest => "E"
        };
    }
}

A su vez, registramos el HttpClient en Startup.ConfigureServices() donde además configuramos la dirección base del servidor.

public void ConfigureServices(IServiceCollection services)
{
    // Other registrations...
    services.AddHttpClient<StockService>(client =>
    {
        // For demo only. Use settings instead
        client.BaseAddress = new Uri("https://api.facutherock.net");
    });
}

Necesitamos asegurarnos de que este servicio se comporte acorde a la especificación.

Requerimientos

Queremos escribir buenos test unitarios para asegurarnos que el servicio StockService.IsAvailableAsync() cumpla con los siguientes requerimientos:

  1. Retornar "N" si el parámetro check es igual a false.
  2. Si el parámetro check es igual a true y el resultado de la petición es exitosa (200 – Ok) devolver "S".
  3. Si el parámetro check es igual a true pero el resultado de la petición es de error (400 – BadRequest) devolver "E".

Si bien los requerimientos son muy simples, nos van a servir para demostrar los aspectos que necesitamos.

Desafío

A primera vista podríamos intentar utilizar un HttpClient directamente, cómo en el siguiente ejemplo:

[Fact]
public async Task BadTest()
{
    // Arrange
    var httpClient = new HttpClient();
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(true);

    // Assert
    Assert.Equal("S", result);
}

Sin embargo, ese test fallaría ya que intentaría hacer una petición a un servidor inválido. Incluso si no fallase, implicaría una petición real, lo cuál no es recomendable para un test unitario.

El desafío consiste en crear una versión del HttpClient que simule hacer la petición, pero que realmente no lo haga.

Aspectos técnicos

La clase HttpClient realiza las peticiones utilizando un HttpMessageHandler de base, el cuál expone un método llamado SendAsync.

La manera en la que podemos simular determinados comportamientos es sobrescribiendo el método anterior. Para esto necesitamos generar nuestra propia subclase.

Podremos inyectar nuestra subclase utilizando una de las sobrecargas que la clase HttpClient tiene aceptando una instancia de HttpMessageHandler.

Tiempo de codear

Vamos a empezar a escribir los test necesarios para asegurar el buen funcionamiento de nuestro servicio.

La estrategia consiste en ir generando subclases de HttpMessageHandler que respondan siempre de la misma forma y cuya respuesta sea controlada por nosotros.

Requerimiento 1

El test para este requerimiento es el más simple de implementar, ya que en este caso no necesitamos utilizar el HttpClient para la petición.

Podemos utilizar directamente una nueva instancia de HttpClient:

[Fact]
public async Task FirstRequirementTest()
{
    // Arrange
    var httpClient = new HttpClient();
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(false);

    // Assert
    Assert.Equal("N", result);
}

Requerimiento 2

Este test es un poco más complejo, ya que necesitamos que la instancia de HttpClient devuelva un mensaje de retorno exitoso.

Para esto, crearemos una subclase de HttpMessageHandler y sobrescribiremos el método SendAsync para devolver siempre un resultado exitoso.

public class OkHttpMessageHandler : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var okResult = new HttpResponseMessage
        {
            StatusCode = System.Net.HttpStatusCode.OK
        };

        return Task.FromResult(okResult);
    }
}

Finalmente crearemos una instancia de un HttpClient y completaremos el test.

[Fact]
public async Task SecondRequirementTest()
{
    // Arrange
    var okHttpMessageHandler = new OkHttpMessageHandler();
    using var httpClient = new HttpClient(okHttpMessageHandler)
    {
        BaseAddress = new Uri("http://fake.server")
    };
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(true);

    // Assert
    Assert.Equal("S", result);
}

Requerimiento 3

Este test es exactamente al anterior, solo que la respuesta del HttpClient de ser de error.

public class BadRequestHttpMessageHandler : HttpMessageHandler
{
    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        var badRequestResult = new HttpResponseMessage
        {
            StatusCode = System.Net.HttpStatusCode.BadRequest
        };

        return Task.FromResult(badRequestResult);
    }
}

Y el test queda de la siguiente forma:

[Fact]
public async Task ThirdRequirementTest()
{
    // Arrange
    var badRequestHttpMessageHandler = new BadRequestHttpMessageHandler();
    using var httpClient = new HttpClient(badRequestHttpMessageHandler)
    {
        BaseAddress = new Uri("http://fake.server")
    };
    var sut = new StockService(httpClient);

    // Act
    var result = await sut.IsAvailableAsync(true);

    // Assert
    Assert.Equal("E", result);
}

Conclusión

Para escribir buenos test unitarios en casos donde se utiliza la clase HttpClient, es necesario tener control total de las respuesta que el mismo retorna.

En este post vimos como utilizar HttpMessageHandler para simular el comportamiento deseado y asegurar un correcto funcionamiento.

Evitar usuarios de redes sociales duplicados en Azure AD B2C (Propuesta)

Azure AD B2C es un excelente servicio para el manejo de inicio de sesión y registro de usuarios que además ofrece integración con redes sociales.

Uno de los aspectos negativos es que no ofrece ningún tipo de mecanismo para unificar usuarios, o al menos para prevenir su duplicación, cuando el email es el mismo.

En este post analizaremos en detalle que herramientas Azure AD B2C nos provee y crearemos una propuesta de arquitectura para detectar usuarios duplicados. En los próximos artículos completaremos la implementación.

Contexto

Dentro de Azure AD B2C existe el concepto de Proveedor de Identidad, el cuál representa el servicio dueño de la identidad virtual de los usuarios.

Ejemplos de proveedores de identidad pueden ser Google o GitHub, los cuáles son externos, ya que son ellos los responsables por la información de los usuarios.

Pero también tenemos la opción de utilizar email y contraseña y que Azure AD B2C se encargue de la identidad. Este proveedor se denomina Local.

Caso de uso

Un usuario opta por registrarse mediante el proveedor local (con email y contraseña) con el email usuario@gmail.com.

Sin embargo, más tarde inicia sesión con GitHub utilizando el mismo email. Es bastante frecuente que el usuario olvide el primer inicio de sesión.

Si bien se utilizaron dos proveedores de identidad diferentes (Local y GitHub), la identidad de usuario es la misma.

Azure AD B2C no ofrece un mecanismo para detectar estos casos, lo que deriva en la creacion de dos perfiles de usuarios diferentes.

Solución

Azure AD B2C crea una identidad de usuario para cada usuario nuevo que se registra, independientemente de qué proveedor se haya utilizado.

Tabla comparativa usuario Local y GitHub
Tabla comparativa usuario Local y GitHub

La solución “ideal” sería unificar las identidades en los casos donde el email es el mismo, sin embargo eso implica escribir “Políticas personalizadas” y es mucho más complejo (Escribiré una serie de posts dedicados exclusivamente a esto).

Una solución parcial, pero eficaz y eficiente, consiste el prevenir la creación de un usuario si ya existe un usuario con el mismo email (sea de un proveedor externo como local).

Conectores de API

Los Conectores de API es una forma de integrar nuestro flujo de usuario con REST APIs para poder realizar validaciones o ejecutar reglas de negocio antes de la creación de un usuario.

Los conectores pueden ser llamados en dos momentos específicos:

  • Después de haber iniciado sesión con un proveedor de identidad externo. Este conector se ejecuta solo para inicios de sesión con proveedores externos y antes de completar el formulario con los atributos de usuario.
  • Antes de crear el usuario. Este conector se ejecuta después de completar el formulario de atributos de usuario y antes de la creación del usuario.
Diagrama de flujo de conectores
Diagrama de flujo de conectores

Los Conectores de API son invocados únicamente durante el registro de un usuario, ya sea con un proveedor externo o local, y no durante el inicio de sesión.

Microsoft Graph API

El equipo de Microsoft ha creado una REST API estándar llamada Graph API la cuál nos va a permitir interactuar con Azure AD B2C.

Una de las caracteristicas que nos interesa es la de poder consultar la base de datos de usuarios de Azure AD B2C.

Propuesta

Hasta el momento tenemos los Conectores de API para ejecutar REST APIs y Microsoft Graph API para obtener información de la base de datos de usuarios.

Nos falta una pieza fundamental para completar la solución: una REST API que sea ejecutada por un Conector e interactúe con Graph API.

Para esto vamos a utilizar Azure Functions. No sólo es muy fácil de integrar y económico, sino también simple de utilizar. Al ser un servicio serverless, no tenemos que preocuparnos por la infraestructura, sino solo de nuestro código.

Propuesta de arquitectura

Nuestra API recibirá como parte de la petición el email con el que se está iniciando sesión y validará contra Microsoft Graph API si ya existe un usuario con ese mismo email en nuestro tenant de Azure AD B2C. En caso de existir devolverá un código de estado de error (400 – Bad Request), caso contrario un código de estado de éxito (200 – Ok).

El Conector de API leerá el código de estado devuelto y continuará con el flujo d ejecución si el código es exitoso, o detendrá el flujo y mostrará al usuario un error si el código no es exitoso.

Próximos Pasos

Ya con la propuesta en pie, estamos en condiciones de comenzar a crear los componentes necesarios para la implementación de la solución.

No te pierdas el próximo post donde vamos a comenzar a crear los Conectores de API junto con una Azure Function base que nos va a permitir entender la propuesta.

Código más testeable con ISystemClock

Te propongo examinar juntos el siguiente fragmento de código:

public class BirthDayCalculator
{
    public int CalculateNextBirthDayInDays(
        int month,
        int day)
    {
        var today = DateTime.UtcNow;
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay -  today).TotalDays;
    }
}

A primera vista se ve bien, dados un mes y un día, el método CalculateNextBirthDayInDays() calcula y retorna la cantidad de días restantes para el próximo cumpleaños.

Sin embargo, algo no huele del todo bien. Ese método no soporta test unitarios.

Problema

Dos de los requesitos estrictamente necesarios de un test unitario, es que debe tener absoluto control del contexto de ejecución y debe ser repetible.

La solución que vimos en la sección anterior, funciona, pero depende de la propiedad estática DateTime.UtcNow. Debido a que no podemos controlar los valores que dicha propiedad puede tomar, no tenemos forma de escribir casos de prueba controlados y repetibles.

Alternativa poco elegante

Una alternativa simple al problema planteado es que el método CalculateNextBirthDayInDays() reciba un parámetro adicional representando la fecha y hora actual. Algo más o menos así:

public class BirthDayCalculator
{
    public int CalculateNextBirthDayInDays(
        int month,
        int day,
        DateTime today)
    {
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay -  today).TotalDays;
    }
}

Esta modificación, poco elegante pero efectiva, nos permite escribir pruebas como la siguiente:

[Fact]
public void CalculateNumberOfDays()
{
    // Arrange
    var sut = new BirthDayCalculator();

    // Act
    var actualDays = sut.CalculateNextBirthDayInDays(
        month: 3,
        day: 13, 
        today: DateTime.UtcNow);

    // Assert
    Assert.Equal(290, actualDays);
}

Esta implementación tiene los siguientes puntos en contra:

  • No tenemos una forma común o servicio para obtener la fecha actual en forma testeable y centralizada, generando casi con seguridad código repetido.
  • Cada vez que es necesario realizar cálculos con fechas, necesitamos agregar un parámetro adicional. Incluso en aquellos casos donde se haga uso de del método CalculateNextBirthDayInDays().
public int ConsumingCode(DateTime today)
{
    int month = 3, day = 13;
    var calculator = new BirthDayCalculator();

    return calculator.CalculateNextBirthDayInDays(
        month,
        day,
        today);
}

ISystemClock al rescate

El equipo de Microsoft ha creado una bonita interfaz junto con una implementación estándar que se encuentra disponible en el paquete Microsoft.AspNetCore.Authentication llamada ISystemClock y SystemClock respectivamente.

Esta interfaz nos permite solucionar los dos puntos anteriores. Por un lado, no necesitamos agregar parámetros extras a todos los métodos, por el otro, nos permite utilizar inyección de dependencias para centralizar una única implementación.

namespace Microsoft.AspNetCore.Authentication
{
    public interface ISystemClock
    {
        DateTimeOffset UtcNow { get; }
    }
}

¿Cómo la usamos?

Vamos a modificar la clase BirthDayCalculator para recibir por parámetro en el constructor mediante inyección de dependencias la interfaz ISystemClock.

Luego simplemente reemplazamos la llamada a DateTime.UtcNow por _systemClock.UtcNow.

public class BirthDayCalculator
{
    private readonly ISystemClock _systemClock;

    public BirthDayCalculator(ISystemClock systemClock)
    {
        _systemClock = systemClock;
    }

    public int CalculateNextBirthDayInDays(
        int month,
        int day)
    {
        var today = _systemClock.UtcNow;
        var birthDay = new DateTime(today.Year, month, day);

        if (birthDay < today)
            birthDay = birthDay.AddYears(1);

        return (int)(birthDay - today).TotalDays;
    }
}

¿Cómo lo testeamos?

Primero generaremos una clase fake, la cuál llamaremos FakeSystemClock, que implemente la interfaz y que reciba mediante un parámetro en el constructor la fecha y hora a retornar.

public class FakeSystemClock : ISystemClock
{
    private readonly DateTime _desiredDay;

    public FakeSystemClock(DateTime desiredDay)
    {
        _desiredDay = desiredDay;
    }

    public DateTimeOffset UtcNow => _desiredDay;
}

Finalmente, modificamos el test para generar una nueva instancia de FakeSystemClock y pasamos esa instancia a nuestro BirthDayCalculator.

[Fact]
public void CalculateNumberOfDays()
{
    // Arrange
    var desiredDay = new DateTime(2021, 5, 26, 10, 0, 0);
    var fakeSystemClock = new FakeSystemClock(desiredDay);
    var sut = new BirthDayCalculator(fakeSystemClock);

    // Act
    var actualDays = sut.CalculateNextBirthDayInDays(
        month: 3,
        day: 13);

    // Assert
    Assert.Equal(290, actualDays);
}

Si estuviésemos utilizando librerías como Moq o FakeItEasy, no necesitaríamos crear esta clase fake.

ISystemClock en Asp.Net Core

Si estamos en el contexto de aplicaciones Asp.Net Core, podemos utilizar la implementación estándar provista como parte del paquete.

Sólo necesitamos registrarla en nuestro contenedor IoC.

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<ISystemClock, SystemClock>();

    // More services
    services.AddControllers();
}

Conclusión

De una forma muy simple y con poco refactoring, podemos eliminar las dependencias a DateTime.UtcNow y hacer nuestro código testeable gracias a ISystemClock.

Podrás encontrar el código completo de ejemplo en mi GitHub.

Azure Days 2021: Azure Web PubSub

Durante los días 21 y 22 de Mayo, se llevó a cabo de forma online el Azure Days 2021 de la mano de ConoSur.Tech.

Es un evento totalmente gratuito y en español donde diversos referentes y expertos de la comunidad ofrecen charlas sobre distintas tecnologías y servicios en Azure.

Este año tuve el enorme placer y honor de participar como speaker dando una charla de un nuevo servicio que promete revolucionar la forma en la que utilizamos WebSockets: Azure Web PubSub.

Estuve hablando sobre todas las funcionalidades y características que nos ofrece este excelente servicio. Además, mostré una pequeña demo de como funciona el servicio y como se integra fácilmente con Azure Functions.

Azure Days 2021 – Azure Web PubSub

Y como un pequeño extra, les dejo la entrevista que me hizo la genia de Ivana Tilca.

Azure Days 2021 – Entrevista

Podrán encontrar el repositorio de la demo con todo el código de los ejemplos aquí.

Manejo de excepciones sin excepciones con .NET 5

Uno de los aspectos más importantes en el desarrollo de aplicaciones es el manejo de excepciones. Esto es verdadero independientemente del tipo de aplicación o plataforma.

Es habitual que incluso diseñemos reglas de negocio en forma de excepciones, por ejemplo para validar parámetros de entrada o ejecutar acciones no permitidas sobre algunas entidades de dominio.

Algo negativo o falla de diseño que tiene esta forma de manejar excepciones es que no es para nada declarativa. C# no provée ningún mecanismo para hacer explícito qué excepciones puede arrojar un método.

En esta sesión veremos una alternativa para solucionar esa falla de diseño apoyándonos en una excelente librería llamada OneOf.

Manejo de excepciones sin excepciones

Como siempre, podrán encontrar el código completo de todos los ejemplos en mi GitHub.

Personalización de vistas en Azure AD B2C

Azure AD B2C es un servicio brillante que nos permite generar flujos para inicio de sesión, registro, recupero de contraseña, etc., en forma muy simple y rápida sin necesidad de escribir código.

Sin embargo, en ocasiones es necesario implementar nuestra propia hoja de estilos de forma que los usuarios tengan una experiencia totalmente integrada.

En este artículo veremos como personalizar nuestras vistas aplicando hojas de estilos propias.

Contexto

Cuándo desarrollamos aplicaciones web, uno de los factores determinantes para el éxito o el fracaso de nuestra aplicación es la experiencia de usuario o UX.

Un estilo o diseño consistente favorece la experiencia de usuario haciendo la utilización más intuitiva y una navegación más fluida.

Si bien Azure AD B2C ofrece flujos de usuario con vistas listas para usar y diseños estándares, puede que no sean consistentes con el resto de nuestra aplicació y necesitemos implementar nuestra propia hoja de estilos.

Requerimiento

Ya tenemos configurado nuestro flujo de usuario unificado para registro o inicio de sesión incluyendo algunos proveedores externos.

Inicio de sesión estándar
Inicio de sesión estándar

Ahora necesitamos que la lista de proveedores externos se muestre primero, es decir, antes de la opción con Email y Contraseña.

Para lograr implementar este requerimiento es necesario realizar algunas modificaciones al código css.

¿Cómo funciona?

Cada flujo de usuario está compuesto por una serie de vistas. A su vez, cada vista tiene asociado un archivo de estilos particular.

Desde el panel del flujo de usuario, navegaremos a la sección Diseños de Página para acceder a la lista de vistas que componen el flujo actual.

Lista de diseños de páginas
Lista de diseños de páginas

Como podemos apreciar en la imagen anterior, cada diseño de página tiene la opción de utilizar contenido personalizado, y en el caso de activar esta opción podremos indicar la URI de la página personalizada.

Página personalizada

Para poder utilizar nuestra página personalizada podemos usar un archivo *.html estático, o también páginas dinámicas como .NET, NodeJS o PHP.

El código HTML puede contener cualquier elemento, el único requerimiento es un elemento div con id igual a "api", como en el siguiente ejemplo:

<!DOCTYPE html>
<html>
<head>
</head>
<body>
    <div id="api"></div>
</body>
</html>

Azure AD B2C agregará todos los elementos necesarios dentro del div mencionado anteriormente.

Dentro de este HTML podemos incluir referencias a archivos *.css o *.js, el único requerimiento es que estos archivos sean accesibles públicamente mediante HTTPS.

Plantillas predefinidas

Si bien tenemos la opción de utilizar HTML totalmente personalizado, Azure AD B2C nos ofrece plantillas completas que podemos utilizar como base.

Sobre estas plantillas base haremos nuestros cambios. Esto hace mucho más simple la personalización.

Podemos descargar las platillas base desde aquí.

Hosting

Las plantillas personalizadas que crearemos deben poder ser accedidas en forma pública mediante una petición GET HTTPS.

Podemos utilizar cualquier tipo de servicio a tal fin, como Azure Web Apps, Azure Functions, una aplicación web Node.js, etc.

Para este ejemplo utilizaremos un archivo *.html estático almacenado en un blob de Azure Storage.

Queda fuera del alcance de este artículo la configuración de Azure Storage. Puedes visitar la documentación oficial para encontrar instrucciones paso a paso al respecto.

Prueba inicial

Para entender cómo funcionan las plantillas personalizadas comenzaremos haciendo una prueba muy simple. Crearemos un archivo llamado index.html cuyo único contenido será un Título de página.

Este archivo no contendrá ningún estilo css ni ningún otro elemento html, solo el div con id "api" y un elemento title:

<!DOCTYPE html>
<html>
<head>
    <title>Prueba inicial</title>
</head>
<body>
    <div id="api"></div>
</body>
</html>

Debemos subir este archivo a Azure Storage y asegurarnos que es de público acceso. Al finalizar, tomar nota de la URI de acceso.

Finalmente, actualizaremos el diseño de página para utilizar como plantilla personalizada la URI que copiamos en el paso anterior.

Luego de guardar los cambios y ejecutar el flujo de usuario, deberíamos obtener el siguiente resultado:

Inicio de sesión sin estilo
Inicio de sesión sin estilo

Implementación del requerimiento

Para poder implementar el requerimiento, vamos a partir de la plantilla por defecto que Azure AD B2C utiliza. Podemos obtener este diseño desde la misma vista donde configuramos la vista personalizada.

Una vez descargada la plantilla, podemos hacer las modificaciones que necesitamos sobre la misma en función de nuestras necesidades.

Para implementar satisfactoriamente nuestro requerimiento, necesitamos alterar el estilo del elemento div con id "api" de forma que ordene los elementos interiores de forma inversa, para eso combinaremos las propiedades display: flex y flex-direction: column-reverse.

Agregaremos un atributo style al elemento con el valor "flex-direction: column-reverse; display: flex;" como se muestra a continuación:

<html>
<head>
  <title>Sign up or sign in</title>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link href="https://ajax.aspnetcdn.com/ajax/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" type="text/css">
  <style id="common">...</style>
  <style id="idpselector">...</style>
  <style>...</style>
  <style>...</style>
  <script type="text/javascript" src="chrome-extension://lbdfknhegglkmkgffjecdpodddfnibpb/inject.js"></script>
</head>
<body data-new-gr-c-s-check-loaded="14.1010.0" data-gr-ext-installed="">
  <div id="background_branding_container" data-tenant-branding-background-color="true">
    <img data-tenant-branding-background="true" ...>
  </div>
  <div class="container  unified_container " role="presentation">
      <div class="row">
          <div class="col-lg-6">
              <div class="panel panel-default">
                  <div class="panel-body">
                      <img class="companyLogo" data-tenant-branding-logo="true" ...>
                      <div id="api" style="flex-direction: column-reverse;display: flex;">
                      </div>
                  </div>
              </div>
          </div>
      </div>
  </div>
  <div id="mfa_inject_cartdata" hidden="hidden"></div>
  <div id="mfa_inject" hidden="hidden"></div>
</body>
</html>

Una vez realizado el cambio en el archivo, lo subiremos a Azure Storage como hicimos anteriormente y guardamos la URI correspondiente.

Una vez guardados los cambios, ejecutaremos el flujo de usuario nuevamente. Esta vez la lista de proveedores externos aparecerá primero, antes de la opción Email y Contraseña.

Inicio de sesión con diseño modificado
Inicio de sesión con diseño modificado

El equipo de desarrolladores de Azure AD B2C puede ir cambiando el html por defecto con el tiempo, en todos los casos donde se requieran personalizaciones como esta, es recomendable analizar cuidadosamente y familiarizarse con la hoja de estilos provista.

Consideraciones

En este ejemplo partimos de una plantilla base e hicimos cambios casi triviales para satisfascer el requerimiento. Sin embargo en ocasiones los cambios serán mucho más complejos.

Cabe destacar que además de poder incluir elementos html adicionales y estilos css, también podemos incluir código JavaScript, ya sea embebido o referenciando archivos externos.

JavaScript

Para poder incluir código JavaScript, ya sea mediante código embebido o archivos externos, necesitamos habilitar su uso desde la sección Propiedades del flujo de usuario.

Habilitar JavaScript
Habilitar JavaScript

Por cuestiones de seguridad, Azure AD B2C tiene ciertas restricciones en cuánto a JavaScript que pueden ir variando con el tiempo. Es recomendable leer atentamente la documentación oficial.

iframes

Los elementos del tipo iframe son considerados inseguros, razón por la cuál no son soportados por Azure AD B2C.

Conclusión

Azure AD B2C nos ofrece la posibilidad de tener un control total sobre la hoja de estilos de cada una de las vistas.

Gracias a este control y la posibilidad de incluir nuestro propio código css, html y js, podremos crear experiencias de usuario totalmente consistentes con el resto de nuestra aplicación.

Integrando Web MVC + Web API .Net 5 con Azure AD B2C

Cuándo trabajamos en ambientes de microservicios, o al menos con distintas APIs, que están protegidos utilizando Azure AD B2C, es necesario que el Front End pueda interactuar con las APIs.

Hoy veremos los pasos necesarios para integrar los tres componentes y tener así nuestro ecosistema de servicios integrados y protegidos con Azure AD B2C.

IMPORTANTE: para poder implementar los pasos aquí mencionados es necesario contar con una configuración básica en Azure AD B2C. Podrás encontrarlos en el siguiente paso a paso.

Antes de Comenzar

En este tutorial vamos a utilizar .Net 5 como marco o framework de desarrollo, con lo cual es un requisito excluyente tener instalado .Net 5 SDK. Si aún no lo tienes, lo puedes descargar aquí, también encontrarás un instructivo paso a paso en caso de que tengas dudas.

Por otro lado, vamos a utilizar Visual Studio 2019 Community Edition, sin embargo este no es un requerimiento excluyente ya que podemos utilizar cualquier editor de texto o ambiente de desarrollo como Visual Studio Code o Notepad++. Puedes descargar Visual Studio 2019 aquí.

Contexto

En los últimos años ha proliferado mucho el desarrollo de aplicaciones compuestas por módulos o APIs independientes del Front End, favoreciendo así la escalabilidad y adaptabilidad a los cambios.

Por supuesto que esto genera un nuevo desafío: Integrar Front Ends y APIs de forma fácil y simple.

Requerimiento

Por un lado tenemos una aplicación Web MVC .Net 5 integrada con Azure AD B2C, por el otro lado tenemos una aplicación Web API .Net 5 que sólo acepta peticiones de usuarios que hayan sido autenticados por Azure AD B2C.

Necesitamos realizar los cambios necesarios para que nuestra aplicación MVC pueda comunicarse con nuestra API.

Solución

Nuestra API solo acepta peticiones de usuarios autenticados por Azure AD B2C, para esto utiliza JWT tokens. Estos tokens contienen la información suficiente para asegurar que se trata de un usuario legítimo.

La API espera recibir con cada petición una cabecera o header de autenticación incluyendo un JWT token emitido por Azure AD B2C.

La solución consiste en configurar los mecanismos necesarios para que la aplicación MVC pueda generar JWT tokens en nombre del usuario y así hacer las peticiones a la API en forma correcta.

Paso 1: Preparar la aplicación Web MVC

Lo primero que haremos es preparar nuestra aplicación Web MVC para mostrar los resultados obtenidos de la API, esto implica crear un Modelo de Vista o View Model, un Controlador y una Vista..

Modelo de Vista o View Model

Para poder mostrar el resultado obtenido de la API necesitamos un View Model, para lo cuál crearemos una nueva clase llamada WeatherForecastViewModel.cs bajo el directorio Models:

public class WeatherForecastViewModel
{
    public DateTime Date { get; set; }

    public int TemperatureC { get; set; }

    public int TemperatureF { get; set; }

    public string Summary { get; set; }
}

El View Model que acabamos de crear se corresponde a la plantilla que utiliza Visual Studio por defecto.

Controlador

Por el momento el controlador tendrá la tarea de comunicarse con la API y devolver los resultados a la vista. Los pasos son los siguientes:

  1. Hacer la petición a la API
  2. Leer y deserializar el resultado
  3. Devolver el resultado a la vista.

Crearemos un nuevo Controlador llamado WeatherController dentro del directorio Controllers. Este controlador tendrá una sola acción:

public class WeatherController : Controller
{
    private readonly HttpClient _httpClient;

    public WeatherController(HttpClient httpClient) =>
        _httpClient = httpClient;

    public async Task<IActionResult> Index()
    {
        var response = await _httpClient.GetAsync("https://localhost:5002/weatherforecast");
        var content = await response.Content.ReadAsStringAsync();
        var values = JsonConvert.DeserializeObject<IEnumerable<WeatherForecastViewModel>>(content);

        return View(values);
    }
}

Debido a que la API está protegida, este controlador todavía no va a funcionar. Modificaremos en los próximos pasos el controlador para poder realizar la interacción con la API en forma exitosa.

Además de incluir algunos usings que serán sugeridos por Visual Studio, es necesario instalar un paquete adicional para la deserialización: Newtonsoft.Json.

Vista

Para mostrar los resultados crearemos una nueva vista llamada Index.cshtml, pero antes debemos crear un nuevo directorio llamado Weather dentro del directorio Views.

Directorio de vistas
Directorio de vistas

El código de la vista estará compuesto simplemente por una tabla mostrando los resultados obtenidos en el controlador.

@model IEnumerable<WeatherForecastViewModel>
@{
    ViewData["Title"] = "Weather";
}

<h1>Weather</h1>
<table class="table">
    <thead>
        <tr>
            <th>@Html.DisplayNameFor(model => model.Date)</th>
            <th>@Html.DisplayNameFor(model => model.TemperatureC)</th>
            <th>@Html.DisplayNameFor(model => model.TemperatureF)</th>
            <th>@Html.DisplayNameFor(model => model.Summary)</th>
        </tr>
    </thead>
    <tbody>
        @foreach (var item in Model)
        {
            <tr>
                <td>@Html.DisplayFor(modelItem => item.Date)</td>
                <td>@Html.DisplayFor(modelItem => item.TemperatureC)</td>
                <td>@Html.DisplayFor(modelItem => item.TemperatureF)</td>
                <td>@Html.DisplayFor(modelItem => item.Summary)</td>
            </tr>
        }
    </tbody>
</table>

Como último paso debemos agregar un acceso directo a esta nueva vista en la cabecera general.

Para esto agregaremos uno nuevo elemento a la sección Header de la vista compartida llamada _Layout.cshtml que se encuentra dentro del directorio Views/Shared:

<ul class="navbar-nav flex-grow-1">
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Privacy">Privacy</a>
    </li>
    <li class="nav-item">
        <a class="nav-link text-dark" asp-controller="Weather" asp-action="Index">Weather</a>
    </li>
</ul>
<partial name="_LoginPartial" />

En este punto ya tenemos la vista preparada, si ejecutamos el proyecto MVC deberíamos ver una nueva entrada en la cabecera. Sin embargo, si intentamos navegar hasta esta nueva vista, obtendremos un error.

Este error se debe a que todavía, nuestra aplicación no puede generar el token necesario para interactuar con la API.

Pantalla de bienvenida
Pantalla de bienvenida

Paso 2: Configuración en Azure AD B2C

Hasta el momento configuramos en nuestro tenant de Azure AD B2C un registro de aplicación para la aplicación MVC y otro registro de aplicación para la API.

Además, cuando registramos la API creamos un ámbito o scope llamado Lector que se ve asi:

https://linkedinazureadb2c.onmicrosoft.com/posts/Lector

Bien, ahora necesitamos configurar nuestra aplicación MVC en Azure AD B2C para que pueda solicitar JWT tokens incluyendo el scope de la API. Para esto necesitamos realizar lo siguiente:

  1. Generar un secreto de cliente o client secret
  2. Asignar permisos para utilizar el scope

Generar secreto de cliente

Para generar un secreto tenemos que ir al registro de aplicación correspondiente a la aplicación MVC, y luego en sección Certificados y secretos crear un nuevo secreto de cliente.

Certificados y secretos
Certificados y secretos

Sólo necesitamos darle una descripción, podemos dejar el campo Expiración en su valor por defecto que es de 6 meses. Una vez creado, podremos obtener el valor creado por Azure desde la de secretos utilizando el botón para copiar.

El valor del secreto solo se puede ver y/o copiar por única vez al momento de su creación. Es importante tomar nota del mismo o tendremos que crear uno nuevo.

Asignar permisos para utilizar el scope

Desde el registro de aplicación MVC iremos a la sección Permisos de API y agregaremos un nuevo permiso.

Agregar permiso de API
Agregar permiso de API

Luego navegamos a la pestaña Mis API y seleccionamos del listado la API que habíamos creado para proteger nuestra Web API .Net 5.

Finalmente seleccionamos el permiso Lector que creamos cuando creamos el scope y agregamos el permiso.

Como último paso para terminar de dar los permisos necesarios es conceder consentimiento de administrador. Desde la misma sección de Permisos de API veremos el nuevo permiso pero en estado No concedido.

Concedemos el permiso y terminamos. El icono pasara de naranja a verde como se muestra en la imagen con los otros permisos.

Conceder consentimiento de administrador
Conceder consentimiento de administrador

Paso 3: Configurar aplicación Web MVC

Habiendo realizado todas las configuraciones pertinentes en torno a Azure AD B2C, podemos proceder con la configuración de la aplicación MVC.

Comenzaremos agregando el secreto de cliente que configuramos en el paso anterior al archivo appsettings.json, dentro de la sección "AzureAdB2C".

"AzureAdB2C": {
  "Instance": "https://linkedinazureadb2c.b2clogin.com",
  "ClientId": "00000000-0000-0000-0000-000000000000",
  "Domain": "linkedinazureadb2c.onmicrosoft.com",
  "SignUpSignInPolicyId": "B2C_1_InicioSesionConRegistro",
  "ClientSecret": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}

Finalmente debemos configurar los servicios necesarios para poder generar los JWT tokens necesarios y así poder interactuar con la API.

Para esto, en el método ConfigureServices de la clase Startup.cs utilizaremos los métodos de extensión EnableTokenAcquisitionToCallDownstreamApi() indicando la lista de scopes habilitados y AddInMemoryTokenCaches().

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllersWithViews()
        .AddMicrosoftIdentityUI();

    services.AddMicrosoftIdentityWebAppAuthentication(Configuration, "AzureAdB2C")
        .EnableTokenAcquisitionToCallDownstreamApi(new string[] { "https://linkedinazureadb2c.onmicrosoft.com/posts/Lector" })
        .AddInMemoryTokenCaches();

    services.AddRazorPages();
}

Paso 4: Generar JWT tokens

En el paso anterior configuramos los servicios necesarios para poder generar los JWT tokens que necesitamos incluir en las peticiones a nuestra API.

Uno de esos servicios es ITokenAcquisition, el cuál nos habilita una serie de métodos para obtener de forma muy sencilla distintos tipos de JWT tokens.

Comenzaremos recibiendo en el constructor del controlador mediante inyección de dependencias el servicio y guardándolo en una variable local. Tendremos que agregar la referencia a Microsoft.Identity.Web en la lista de usings.

private readonly HttpClient _httpClient;
private readonly ITokenAcquisition _tokenAcquisition;

public WeatherController(
    ITokenAcquisition tokenAcquisition,
    HttpClient httpClient)
{
    _httpClient = httpClient;
    _tokenAcquisition = tokenAcquisition;
}

Luego modificaremos la acción Index() del controlador. Utilizaremos el método GetAccessTokenForUserAsync() del servicio ITokenAcquisition para obtener un JWT token en nombre del usuario actual indicando la lista de scopes que queremos incluir.

Por último, solo necesitamos incluir el token como nuevo header de autenticación en la petición a la API.

public async Task<IActionResult> Index()
{
    var accessToken = await _tokenAcquisition.GetAccessTokenForUserAsync(new[] { "https://linkedinazureadb2c.onmicrosoft.com/posts/Lector" });
    _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
    _httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

    var response = await _httpClient.GetAsync("https://localhost:5002/weatherforecast");
    var content = await response.Content.ReadAsStringAsync();
    var values = JsonConvert.DeserializeObject<IEnumerable<WeatherForecastViewModel>>(content);

    return View(values);
}

Paso 5: Pruebas

Si llegamos a este punto y no hemos olvidado ningún paso ni configuración, estamos listos para compilar y ejecutar la solución.

Luego de iniciar sesión, podemos verificar que todo funcione correctamente haciendo clic en la nueva acción disponible en la cabecera. Deberíamos ver el resultado devuelto por la API en forma de tabla.

Resultado en forma de tabla
Resultado en forma de tabla

Si intentamos acceder a la vista sin haber iniciado sesión obtendremos una excpción del tipo MsalUiRequiredException.

Es nuestra responsabilidad detectar este escenario y redireccionar al usuario a la pantalla de inicio de sesión.

Conclusión

Con este post hemos completado una integración completa de una aplicación Web MVC, una Web API y Azure AD B2C, y vimos que es muy simple e intuitivo la forma en que los tres componentes se integran.

Gracias a las librerías Microsoft.Identity.Web y Microsoft.Identity.Web.UI podemos escribir muy poco código y reducir así el esfuerzo y tiempo necesarios.

Podrás encontrar el código completo en mi GitHub.

Lanzamiento de Azure Web PubSub (Preview)

En la última semana de Abril, Microsoft anunció el lanzamiento en modo preview de un nuevo servicio llamado Azure Web PubSub.

En este artículo veremos cuáles son las características principales, posibles casos de uso y costos de este nuevo servicio que promete ser el nuevo de facto para comunicación de aplicaciones en tiempo real.

Mensajería en tiempo real

Azure Web PubSub es un servicio totalmente manejado (no requiere la gestión de componentes de infraestructura) diseñado para la construcción de aplicaciones que requieren mensajería en tiempo real con soporte nativo para WebSockets.

Una de las claves de WebSockets es que soporta comunicación dual bidireccional (full-duplex) entre clientes y servidores utilizando TCP. Azure Web PubSub simplifica todo el manejo relacionado a los servidores WebSockets.

Es por esto que Azure Web PubSub permite crear aplicaciones con mensajería en tiempo real en forma ágil, eficiente, escalable y simple.

Características principales

Una de las características principales es que Azure Web PubSub está preparado para arquitecturas de gran escala y con alta disponibilidad, siendo un servicio robusto y seguro.

Otra característica interesante es la integración en forma nativa con Azure Functions, lo que nos permite modelar arquitecturas totalmente serverless.

En cuánto a lo que respecta a lenguajes de programación para la creación de servidores, al momento existen SDKs para C#, Java, JavaScript y Python.

Azure Web PubSub vs Azure SignalR

Ambos servicios tienen cómo propósito simplificar la construcción y manejo de sistemas de mensajería en tiempo real entre clientes y servidores. Además ambos utilizan WebSockets.

Sin embargo, Azure SignalR está basado en una serie de librerías de SignalR que requieren algunos componentes específicos, qué en muchos casos no están disponibles en el cliente. Por el contrario, Azure Web PubSub sólo necesita WebSockets.

Casos de Uso

Vista general de Azure Web PubSub
Vista general de Azure Web PubSub

La lista de posibles casos de uso puede ser ilimitada, o al menos muy extensa. Pero aquí tenemos algunos casos de uso comunes:

  • Actualizaciones altamente frecuentes: como juegos o subastas.
  • Chats: aplicaciones que requieren algún tipo de interacción entre usuarios (vendedores con clientes por ejemplo)
  • Estado en tiempo real de distintos tipos de dispositivos: incluyendo no sólo dispositivos IoT sino también smartphones y computadoras.

Precio

El servicio se factura en función de la cantidad de unidades y de la cantidad de mensajes mensuales.

  • Unidades: Cada unidad representa una cantidad máxima de conexiones concurrentes. Así por ejemplo, a mayor cantidad de unidades mayor cantidad de conexiones concurrentes
  • Mensajes: los mensajes se miden en kilobytes de salida y se estiman en 2KB por mensaje. De esta forma se calcula estimativamente que 1000 mensajes equivalen a 2000KB

El servicio se encuentra disponible en dos opciones, Free y Standard. Un aspecto destacable es que el servicio es 100% pago por uso y no implica un costo inicial.

A continuación una tabla comparativa:

FreeStandard
Conexiones concurrentes por unidad201000
Mensajes por unidad por día20000Sin límites (el primer millón de mensajes es gratis)
Precio por unidad por díaGratisUSD 1.61
Cantidad máxima de unidades1100
Tabla comparativa Free vs Standard

Podrás encontrar información más detallada en la aquí.

Conclusión

Azure Web PubSub es nuevo servicio diseñado para comunicación de aplicaciones en tiempo real que promete ser eficiente, escalable, seguro y ágil.

Si querés conocer más sobre el servicio, la documentación oficial es excelente.

« Entradas anteriores

© 2021 Facu The Rock

Tema por Anders NorenArriba ↑