Las ventajas que te ofrece Microsoft Azure y el mundo.NET

Cómo configurar nuestro Identity Server (y nuestras aplicaciones web) detrás de un Load Balancer o un Api Gateway

Hoy en día, lo normal en un despliegue en Producción, es que se disponga de un balanceador de carga o un api proxy que actúen como punto de entrada de las peticiones recibidas y se encarguen de enrutar dichas solicitudes a nuestras aplicaciones web. En este artículo veremos cómo configurar nuestro Identity Server (y nuestras aplicaciones web) para que estén preparadas en ese entorno, y también veremos cómo configurar SSL Offload (requerido en muchas infraestructuras).

Vamos al lío

Para empezar, tenemos que configurar el servicio de Identity Server a través de clase IdentityServerOptions en nuestro método ConfigureServices.

services.AddIdentityServer(options =>
		{ 
			options.PublicOrigin = "https://myurl.loadbalancer.com";
			options.IssuerUri = "https://myurl.loadbalancer.com";
		})
		.AddOperationalStore(options =>;
                              options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
		.AddConfigurationStore(options =>
                              options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
		.AddAspNetIdentity<ApplicationUser>()
		.AddDeveloperSigningCredential();

Veamos en detalle las dos opciones que hemos establecido:

IssuerUri

  • Es el nombre único de nuestra instancia de Identity Server. Por defecto la URL base se toma del servidor donde se encuentre instalado.

PublicOrigin

  • Por defecto, Identity Server emplea el host, el protocolo y el puerto de la petición HTTP para generar los enlaces del documento de configuración de OpenId (esto es importante tenerlo en cuenta para los puntos siguientes). En un escenario en el que Identity Server se encuentra detrás de un balanceador de carga o un api Gateway, provoca que no sea correcto. Para ello se puede sobrescribir el origen utilizado para la generación de enlaces usando esta propiedad.

Con ésto, si iniciamos nuestro Identity Server y nos posicionamos en la siguiente URL/ .well-known/openid-configuration, se mostrará la nueva configuración.

 

Como se puede observar, tanto el issuer como las URLs de los endpoints expuestos por Identity Server son diferentes a la URL del host (en este caso https://localhost:44302).

Un poco de teoría, cabeceras X-Forwarded

Continuando con la configuración de Identity Server vamos a explicar ahora en que consisten las cabeceras X-Forwarded-*.

  • X-Forwarded-For: El campo del encabezado HTTP X-Forwarded-For (XFF) es un método común para identificar la dirección IP de origen de un cliente que se conecta a un servidor web a través de un proxy inverso HTTP o un balanceador de carga.

El formato de esta cabecera es el siguiente

  1. X-Forwarded-For: <client1, proxy1, proxy2, …
  2. Ejemplo: X-Forwarded-For: 129.78.138.66, 129.78.64.103
  • X-Forwarded-Proto: El campo del encabezado HTTP X-Forwarded-Proto es un método común para identificar el protocolo de origen de una solicitud HTTP, ya que un proxy inverso o un balanceador de carga, puede comunicarse con un servidor web utilizando HTTP, incluso si la solicitud al proxy inverso es HTTPS.

El formato de esta cabecera es el siguiente

  1. X-Forwarded-Proto: <protocol>
  2. Ejemplo: X-Forwarded-Proto: https
  • X-Forwarded-Port: El campo del encabezado HTTP X-Forwarded-Port es un método común para identificar el puerto original solicitado por el cliente en el encabezado de solicitud HTTP del host, ya que el nombre de host y / o el puerto del proxy inverso HTTP o balanceador de carga pueden diferir del servidor de origen que maneja la solicitud.

El formato de esta cabecera es el siguiente

  1. X-Forwarded-Port: <number>
  2. Ejemplo: X-Forwarded-Port: 443
  • X-Forwarded-Host: El campo del encabezado HTTP X-Forwarded-Host es un método común para identificar el host original solicitado por el cliente en el encabezado de solicitud HTTP del host, ya que el nombre de host y / o el puerto del proxy inverso HTTP o balanceador de carga pueden diferir del servidor de origen que maneja la solicitud.

El formato de esta cabecera es el siguiente

  1. X-Forwarded-Host: <host>
  2. Ejemplo: X-Forwarded-Host: myurl.loadbalancer.com

Ya conocemos las cabeceras que debe exponer el balanceador de carga o el proxy. La siguiente configuración aplica tanto a nuestro Identity Server como a nuestras aplicaciones web que van a autenticarse contra él y también están detrás del balanceador.

En nuestro método ConfigureServices aplicamos la siguiente configuración (al inicio del método o antes de .AddIdentityServer)

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.Configure<ForwardedHeadersOptions>(options =>
    {
         options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
    });
         …
        services.AddIdentityServer(options =>
    { 
       options.PublicOrigin = "https://myurl.loadbalancer.com";
	options.IssuerUri = "https://myurl.loadbalancer.com"; 
    })
    .AddOperationalStore(options =>
                              options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
		.AddConfigurationStore(options =>
                              options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sqlOptions => sqlOptions.MigrationsAssembly(migrationsAssembly)))
		.AddAspNetIdentity<ApplicationUser>()
		.AddDeveloperSigningCredential();
         …
}

En nuestro método Configure aplicamos la siguiente configuración (al inicio del método o antes de .UseIdentityServer)

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    var fordwardedHeaderOptions = new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto   
    };
            
    fordwardedHeaderOptions.KnownNetworks.Clear();
    fordwardedHeaderOptions.KnownProxies.Clear();

    app.UseForwardedHeaders(fordwardedHeaderOptions);
         …
        app.UseIdentityServer();
         …
}

Bien, ya tenemos configurado nuestro Identity Server para colocarlo detrás de cualquier balanceador de carga o proxy … ¿o no?

No se vayan todavía, aún hay más

Lo siguiente que vamos a contar es fruto de una lucha con Azure Api Gateway en la que nos hemos enfrascado hace poco.

Azure Api Gateway mola mucho, pero tiene un pequeño gran inconveniente, NO genera la cabecera X-Forwarded-Host, sino que devuelve una cabecera X-ORIGINAL-HOST (ver Add the X-Forwarded-Host header to Application Gateway), por lo que cuando se configura una aplicación web para que autentique usuarios vía Identity Server, a la hora de generar la URL de redirección, ésta se generaba de la siguiente forma:

https://myurl.loadbalancer.com/connect/authorize?
client_id=myClientMVC&amp;
redirect_uri=https://10.124.1.4/signin-oidc&amp;
response_type=code id_token&amp;
scope=openid profile&amp;
response_mode=form_post&amp;
nonce=636801444519810075.MTcwNjg1YzctNTAyZC00NDkxLWIxM2EtOWVhMGUzM2U4ODgzYmYzMGY1NDEtZDE0Yy00YTFkLWJhMjMtODhiMWNmNDQwMzdm&amp;state=CfDJ8O7U_mOnuzpOo4VdJnNW0qCKkrd5eDfkxIO1GzUdULyVoiSQfIXkVst6PdpilljuA8KBh2kBROZ3hoG_K8i1V-fgIjcRs23mHuqbowRnd77dKzE8cZRt0VJuvE1Rns8jPcPLUXrCnj76TekCtZshgXw8HiL6dUSWSl2CqigtxBhcZY0p70m_2vYe80xORrpcc59MFsrAlX94z_UkNsacUsEmFbzKvgHVLS5VKT677gp7R284N6Q6Y6Ty15CDzXN1XQgqGxprVp3CDDWS7JaPKxXs5puyqgYHTjeuOVrqNFhJhi4ErLjplej9TNrBVfwtbQ&amp;x-client-SKU=ID_NET&amp;x-client-ver=2.1.4.0

Si os fijáis, la URL de redirección es la IP del host de la aplicación web. ¿Cómo solventar este problema? Pues es muuuuuy sencillo. Aprovechando que Asp.Net Core todo son Middlewares, simplemente después de configurar las cabeceras X-Forwarded-* se aplica el siguiente truquito.

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    var fordwardedHeaderOptions = new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
    };
    fordwardedHeaderOptions.KnownNetworks.Clear();
    fordwardedHeaderOptions.KnownProxies.Clear();

    app.UseForwardedHeaders(fordwardedHeaderOptions);

    string XOriginalHost = "X-ORIGINAL-HOST";

    app.Use((context, next) =>
        {
            if (context.Request.Headers.TryGetValue(XOriginalHost, out StringValues originalHostName))
            {
                context.Request.Host = new HostString(originalHostName);
            }

            return next();
        });

         …
}

En el middleware, se analiza la cabecera de la petición y, si existe la cabecera X-ORIGINAL-HOST, se sobrescribe en la petición. Fijaos también que hemos modificado las ForwardedHeadersOptions para recuperar la cabecera X-Forwarded-Host.
Ahora sí que sí ¡lo tenemos!

Another one!

Como colofón, os voy a explicar cómo configurar Identity Server en el caso en que se requiera aplicar SSL Offload (básicamente es que toda comunicación HTTPS se realiza hasta al balanceador de carga y a partir de ahí, la comunicación con las aplicaciones web se realiza vía HTTP)

Para ello se configura el pipeline de ejecución de nuestra aplicación web o de nuestro Identity Server (se requiere .Net Core 2.1 o posterior)

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.Configure<ForwardedHeadersOptions>(options =>
    {
        options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost;
    });

    services.AddHsts(options =>
    {
        options.Preload = true;
        options.IncludeSubDomains = true;
        options.MaxAge = TimeSpan.FromDays(60);
    });

    services.AddHttpsRedirection(options => options.RedirectStatusCode = StatusCodes.Status307TemporaryRedirect);
         …
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    var fordwardedHeaderOptions = new ForwardedHeadersOptions
    {
        ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost
    };
    fordwardedHeaderOptions.KnownNetworks.Clear();
    fordwardedHeaderOptions.KnownProxies.Clear();

    app.UseForwardedHeaders(fordwardedHeaderOptions);

    string XOriginalHost = "X-ORIGINAL-HOST";

    app.Use((context, next) =>
        {
            if (context.Request.Headers.TryGetValue(XOriginalHost, out StringValues originalHostName))
            {
                context.Request.Host = new HostString(originalHostName);
            }

            return next();
        });


    app.UseHttpsRedirection();
         …
}

Conclusiones

En este POST hemos visto que podemos configurar nuestro Identity Server y aplicaciones web en un entorno productivo que se encuentren detrás de un balanceador de carga o proxy. También hemos repasado cómo aplicar el SSL Offload a nuestro Identity Server y algún que otro truquito para Azure Api Gateway…así que, HAPPY CODING!

mm

Sobre Sergio Parra Guerra

Sergio Parra es Ingeniero Técnico en Informática de Sistemas por la UPSAM. Tiene a sus espaldas muchísimas certificaciones entre las cuales Microsoft Certified Professional y ex Microsoft MVP Visual Studio and Development Technologies. Actualmente es un magnífico Software & Cloud Architect en ENCAMINA.
Esta entrada ha sido publicada en .NET. Enlace permanente.
Suscríbete a Piensa en Sofware desarrolla en Colores

Suscríbete a Piensa en Sofware desarrolla en Colores

Recibe todas las actualizaciones semanalmente de nuestro blog

You have Successfully Subscribed!

ENCAMINA, piensa en colores