Autenticación moderna en .NET: OpenID Connect, BFF, SPA

Mientras las tecnologías web continúan avanzando, también lo hacen los métodos y protocolos diseñados para asegurarlas. Los protocolos OAuth 2.0 y OpenID Connect han evolucionado significativamente en respuesta a las amenazas de seguridad emergentes y la creciente complejidad de las aplicaciones web. Los métodos de autenticación tradicionales, que en su momento resultaron efectivos, ahora se están haciendo obsoletos para las Aplicaciones de Página Única (SPAs) modernas, que enfrentan nuevos desafíos de seguridad. En este contexto, el patrón arquitectónico Backend-For-Frontend (BFF) ha emergido como una solución recomendada para organizar las interacciones entre las SPAs y sus sistemas backend, ofreciendo un enfoque más seguro y manejable para la autenticación y la gestión de sesiones. Este artículo explora en profundidad el patrón BFF, demostrando su aplicación práctica a través de una solución mínima implementada con .NET y React. Al finalizar, tendrá una comprensión clara de cómo aprovechar el patrón BFF para mejorar la seguridad y la funcionalidad de sus aplicaciones web.

Contexto Histórico

La historia de OAuth 2.0 y OpenID Connect refleja la evolución continua de las tecnologías de Internet. Vamos a echar un vistazo más de cerca a estos protocolos y su impacto en las aplicaciones web modernas.

Inducido en 2012, el protocolo OAuth 2.0 ha convertidose en un estándar ampliamente adoptado para la autorización. Permite que las aplicaciones terceros obtengan acceso limitado a los recursos de los usuarios sin exponer las credenciales del usuario al cliente. OAuth 2.0 admite varios flujos, cada uno diseñado para adaptarse flexiblemente a diversos casos de uso.

En base a la infraestructura de OAuth 2.0, el protocolo OpenID Connect (OIDC) emergió en 2014, agregando funcionalidades de autenticación esenciales. Proporciona a las aplicaciones cliente una metodología estándar para verificar la identidad del usuario y obtener información básica acerca de él a través de un punto de acceso estandarizado o adquiriendo un token de identidad en formato JWT (JSON Web Token).

Evolución del Modelo de amenaza

Con las capacidades crecientes y la popularidad de los SPAs, el modelo de amenaza para los SPAs también ha evolucionado. Vulnerabilidades como la Inyección de Script entre Sitios (XSS) y la Forgedía de Solicitudes entre Sitios (CSRF) se han vuelto más comunes. Dado que los SPAs a menudo interactúan con el servidor a través de API, almacenar y usar tokens de acceso y tokens de actualización de manera segura ha resultado crucial para la seguridad.

Respondiendo a las demandas de la época, los protocolos OAuth y OpenID Connect continúan evolucionando para adaptarse a los nuevos retos que surgen con las nuevas tecnologías y el creciente número de amenazas. Al mismo tiempo, la constante evolución de las amenazas y la mejora de las prácticas de seguridad significan que los enfoques obsoletos ya no satisfacen los requerimientos de seguridad modernos. Como resultado, el protocolo OpenID Connect ofrece actualmente una amplia gama de capacidades, muchas de las cuales ya se consideran obsoletas y a menudo inseguras. Esta diversidad crea dificultades para los desarrolladores de SPAs en la selección de la manera más apropiada y segura de interactuar con el servidor OAuth 2.0 y OpenID Connect.

En particular, el Flujode Implícito ya puede considerarse obsoleto, y para cualquier tipo de cliente, ya sea una PÁ o una aplicación móvil o una aplicación de escritorio, ahora se recomienda encarecidamente utilizar el Flujode Autorización junto con el Recurso Clave de Intercambio (PKCE).

Seguridad de los PAs modernos

¿Por qué aún se consideran vulnerables los PAs modernos, incluso al utilizar el Flujode Autorización con PKCE? Existen varias respuestas a esta pregunta.

Vulnerabilidades en el código JavaScript

JavaScript es un poderoso lenguaje de programación que juega un papel clave en las Aplicaciones de una Página Única modernas (SPA). Sin embargo, sus capacidades amplias y su prevalencia representan una amenaza potencial. Las PAs modernas construidas sobre bibliotecas y frameworks como React, Vue o Angular usan una gran cantidad de bibliotecas y dependencias. Puede verlas en la carpeta node_modules y el número de estas dependencias puede estar en cientos o incluso miles. Cada una de estas bibliotecas puede contener vulnerabilidades de grados variables de criticalidad, y los desarrolladores de SPA no tienen la capacidad para revisar cuidadosamente el código de todas las dependencias utilizadas. A menudo, los desarrolladores ni siquiera rastrean la lista completa de dependencias, ya que se dependen mutuamente transitoriamente. Incluso desarrollando su propio código con los más altos estándares de calidad y seguridad, no se puede estar completamente seguro de la ausencia de vulnerabilidades en la aplicación final.

Código JavaScript malicioso, que puede ser inyectado en una aplicación de diversas maneras, a través de ataques como el Inyección de Script entre Sitios (XSS) o a través de la compromisión de bibliotecas de terceros, obtiene los mismos privilegios y nivel de acceso a los datos que el código de aplicación legítimo. Esto permite que el código malicioso stele datos de la página actual, interaccione con la interfaz de la aplicación, envíe solicitudes al backend, stele datos del almacenamiento local (localStorage, IndexedDB) y incluso inicie sesiones de autenticación propias, obteniendo sus propios tokens de acceso usando el mismo flujo de Autorización Code y PKCE.

Vulnerabilidad Spectre

La vulnerabilidad Spectre explota características de la arquitectura de procesador moderna para acceder a datos que deberían estar aislados. Tales vulnerabilidades son particularmente peligrosas para las APS.

En primer lugar, las APS utilizan intensamente JavaScript para gestionar el estado de la aplicación e interactuar con el servidor. Esto aumenta la superficie de ataque para el código JavaScript malicioso que puede explotar vulnerabilidades Spectre. En segundo lugar, a diferencia de las aplicaciones de varias páginas tradicionales (MPAs), las APS rara vez se recargan, lo que significa que la página y su código cargado permanecen activos por un largo tiempo. Esto da a los atacantes mucho más tiempo para realizar ataques usando el código JavaScript malicioso.

Las vulnerabilidades Spectre permiten a los atacantes robar tokens de acceso almacenados en la memoria de una aplicación de JavaScript, permitiendo el acceso a recursos protegidos simulando la aplicación legítima. La ejecución especulativa también puede usarse para robar datos de la sesión del usuario, permitiendo a los atacantes proseguir sus ataques incluso después de que se cierre la APS.

No se puede descartar el descubrimiento de otras vulnerabilidades similares a Spectre en el futuro.

¿Qué Hacer?

Vamos a resumir una importante conclusión interina. Los modernos SPA, dependientes de un gran número de bibliotecas de JavaScript de terceros y ejecutándose en el entorno del navegador en los dispositivos de los usuarios, operan en un entorno de software y hardware que los desarrolladores no pueden controlar completamente. Por lo tanto, debemos considerar que tales aplicaciones son inherentemente vulnerables.

En respuesta a las amenazas enumeradas, muchos expertos se inclinan hacia dejar de almacenar tokens en el navegador y diseñar la aplicación de manera que los tokens de acceso y actualización se obtienen y procesan solo por el lado del servidor de la aplicación, y nunca se pasan al lado del navegador. En el contexto de un SPA con un backend, esto se puede lograr utilizando el patrón Backend-For-Frontend (BFF).

El esquema de interacción entre el servidor de autorización (OP), el cliente (RP) que implementa el patrón BFF y una API de terceros (Servidor de Recursos) se parece a este:

El uso del patrón BFF para proteger SPAs ofrece varias ventajas. Los tokens de acceso y actualización se almacenan en el lado del servidor y nunca se pasan al lado del navegador, evitando así su robo debido a vulnerabilidades. La gestión de la sesión y los tokens se maneja en el servidor, lo que permite un mejor control de seguridad y una verificación de autenticación más confiable. La aplicación cliente se interactúa con el servidor a través del BFF, lo que simplifica la lógica de la aplicación y reduce el riesgo de ejecución de código malicioso.

Implementar el Patrón Backend-For-Frontend en la Plataforma .NET

Antes de proceder a la implementación práctica de BFF en la plataforma .NET, consideremos sus componentes necesarios y planificemos nuestras acciones. Supongamos que ya tenemos un servidor OpenID Connect configurado y necesitamos desarrollar un SPA que trabaje con un backend, implementar autenticación usando OpenID Connect, y organizar la interacción entre las partes del servidor y el cliente usando el patrón BFF.

De acuerdo con el documento OAuth 2.0 para Aplicaciones Basadas en Navegador, el patrón arquitectónico BFF supone que el backend actúa como cliente de OpenID Connect, utiliza el flujo de código de autorización con PKCE para la autenticación, obtiene y almacena tokens de acceso y de actualización en su lado, y nunca los pasa al lado del SPA en el navegador. El patrón BFF también supone la presencia de una API en el lado del backend que consta de cuatro puntos finales principales:

  1. Check Session: sirve para verificar una sesión de autenticación de usuario activa. Normalmente es llamado desde el SPA mediante una API asíncrona (fetch) y, si es exitoso, devuelve información sobre el usuario activo. Así, el SPA, cargado desde una tercera fuente (por ejemplo, CDN), puede verificar el estado de autenticación y o continuar su funcionamiento con el usuario o proceder a la autenticación usando el servidor OpenID Connect.
  2. Inicio de sesión: inicia el proceso de autenticación en el servidor OpenID Connect. Normalmente, si el SPA falla al obtener datos de usuario autenticado en el paso 1 a través de Check Session, redirige el navegador a esta URL, que a su vez forma una solicitud completa al servidor OpenID Connect y redirige el navegador allí.
  3. Entrar: recibe el código de autorización enviado por el servidor después del paso 2 en el caso de una autenticación exitosa. Realiza una solicitud directa al servidor OpenID Connect para intercambiar el código de autorización + código verificador PKCE por tokens de acceso y actualización. Inicia una sesión autenticada en el lado del cliente mediante la emisión de una cookie de autenticación al usuario.
  4. Cerrar sesión: se utiliza para finalizar la sesión de autenticación. Normalmente, el SPA redirige el navegador a esta URL, que a su vez forma una solicitud al punto final de sesión en el servidor OpenID Connect para finalizar la sesión, así como elimina la sesión en el lado del cliente y la cookie de autenticación.

Ahora veamos las herramientas que proporciona la plataforma .NET de forma predeterminada y veamos cómo pueden utilizarnos para implementar el patrón BFF. La plataforma .NET ofrece el paquete NuGet Microsoft.AspNetCore.Authentication.OpenIdConnect, que es una implementación lista de un cliente OpenID Connect apoyado por Microsoft. Este paquete admite tanto el flujo de código de autorización como PKCE, y agrega un punto final con la ruta relativa /signin-oidc, que ya implementa la funcionalidad del punto final de inicio de sesión necesaria (ver punto 3 anterior). Así, solo necesitamos implementar los tres puntos finales restantes.

Para un ejemplo práctico de integración, consideraremos un servidor de prueba OpenID Connect basado en la biblioteca Abblix OIDC Server. Sin embargo, todo lo mencionado a continuación se aplica a cualquier otro servidor, incluyendo los servidores disponibles al público de Facebook, Google, Apple y cualquier otro que cumpla con la especificación del protocolo OpenID Connect.

Para implementar el SPA en el lado del frontend, utilizaremos la biblioteca React, y en el lado del backend, utilizaremos .NET WebAPI. Esta es una de las tecnologías más comunes en el momento de escribir este artículo.

El esquema general de componentes y su interacción se parece a este:

Para trabajar con los ejemplos de este artículo, también necesitará instalar el .NET SDK y Node.js. Todos los ejemplos de este artículo fueron desarrollados y probados usando .NET 8, Node.js 22 y React 18, que eran actuales en el momento de la escritura.

Creando un SPA de Cliente en React con un Backend en .NET

Para crear una aplicación cliente rápidamente, es conveniente utilizar una plantilla lista. Hasta la versión .NET 7, el SDK ofrecía una plantilla integrada para una aplicación .NET WebAPI y un SPA de React. Desafortunadamente, esta plantilla fue eliminada en la versión .NET 8. Es por eso que el equipo de Abblix ha creado su propia plantilla, que incluye un backend WebApi de .NET, un frontend SPA basado en la biblioteca React y TypeScript, construido con Vite. Esta plantilla está disponible al público como parte del paquete Abblix.Templates y se puede instalar ejecutando el siguiente comando:

Shell

 

dotnet new install Abblix.Templates

Ahora podemos usar la plantilla llamada abblix-react. Vamos a usarla para crear una nueva aplicación llamada BffSample:

Shell

 

dotnet new abblix-react -n BffSample

Este comando crea una aplicación que consiste en un backend de .NET WebApi y un cliente SPA de React. Los archivos relacionados con el SPA están ubicados en la carpeta BffSample\ClientApp.

Después de crear el proyecto, el sistema le pedirá que ejecute un comando para instalar las dependencias:

Shell

 

cmd /c "cd ClientApp && npm install"

Esta acción es necesaria para instalar todas las dependencias requeridas para la parte cliente de la aplicación. Para un funcionamiento exitoso del proyecto, se recomienda aceptar y ejecutar este comando ingresando Y (sí).

Vamos a cambiar inmediatamente el número de puerto en el que se ejecuta la aplicación BffSample localmente a 5003. Esta acción no es obligatoria, pero simplificará la configuración del servidor OpenID Connect. Para hacer esto, abra el archivo BffSample\Properties\launchSettings.json, busque el perfil llamado https y cambie el valor de la propiedad applicationUrl a https://localhost:5003.

A continuación, agregue el paquete NuGet que implementa el cliente OpenID Connect navegando a la carpeta BffSample y ejecutando el siguiente comando:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Configure dos esquemas de autenticación llamados Cookies y OpenIdConnect en la aplicación, leyendo sus configuraciones de la configuración de la aplicación. Para esto, realice cambios en el archivo BffSample\Program.cs:

C#

 

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// ******************* START *******************
var configuration = builder.Configuration;

builder.Services
    .AddAuthorization()
    .AddAuthentication(options => configuration.Bind("Authentication", options))
    .AddCookie()
    .AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************** END ********************
var app = builder.Build();

Y agregue las configuraciones necesarias para conectarse al servidor OpenID Connect en el archivo BffSample\appsettings.json:

JSON

 

{
  // ******************* START *******************
  "Authentication": {
      "DefaultScheme": "Cookies",
      "DefaultChallengeScheme": "OpenIdConnect"
  },
  "OpenIdConnect": {
      "SignInScheme": "Cookies",
      "SignOutScheme": "Cookies",
      "SaveTokens": true,
      "Scope": ["openid", "profile", "email"],
      "MapInboundClaims": false,
      "ResponseType": "code",
      "ResponseMode": "query",
      "UsePkce": true,
      "GetClaimsFromUserInfoEndpoint": true
  },
  // ******************** END ********************
  "Logging": {
    "LogLevel": {
      "Default": "Information",

Y en el archivo BffSample\appsettings.Development.json:

JSON

 

{
  // ******************* START *******************
  "OpenIdConnect": {
      "Authority": "https://localhost:5001",
      "ClientId": "bff_sample",
      "ClientSecret": "secret"
  },
  // ******************** END ********************
  "Logging": {
    "LogLevel": {
      "Default": "Information",

Veamos brevemente cada configuración y su propósito:

  • Autenticación sección: La propiedad DefaultScheme establece la autenticación por defecto utilizando el esquema Cookies, y DefaultChallengeScheme delega la ejecución de la autenticación al esquema OpenIdConnect cuando el usuario no puede ser autenticado por el esquema predeterminado. Así, cuando el usuario es desconocido para la aplicación, se llamará al servidor OpenID Connect para la autenticación, y después de eso, el usuario autenticado recibirá una cookie de autenticación, y todas las llamadas posteriores al servidor serán autenticadas con ella, sin contactar nuevamente al servidor OpenID Connect.
  • OpenIdConnect

    :

    • Las propiedades SignInScheme y SignOutScheme especifican el esquema Cookies, que se utilizará para guardar la información del usuario después de la autenticación.
    • La propiedad Authority contiene la URL base del servidor de OpenID Connect. ClientId y ClientSecret especifican el identificador y la clave secreta de la aplicación cliente, que se registran en el servidor de OpenID Connect.
    • SaveTokens indica la necesidad de guardar los tokens recibidos como resultado de la autenticación del servidor de OpenID Connect.
    • Scope contiene una lista de ámbitos que la aplicación BffClient solicita acceso. En este caso, se solicitan los ámbitos estándar openid (identificador de usuario), profile (perfil de usuario) y email (correo electrónico).
    • MapInboundClaims es responsable de la transformación de las reclamaciones entrantes del servidor de OpenID Connect en reclamaciones utilizadas en la aplicación. Un valor de false significa que las reclamaciones se guardarán en la sesión del usuario autenticado en la forma en que se reciben del servidor de OpenID Connect.
    • ResponseType con el valor code indica que el cliente utilizará el flujo de código de autorización.
    • ResponseMode especifica la transmisión del código de autorización en la cadena de consulta, que es el método predeterminado para el flujo de código de autorización.
    • La propiedad UsePkce indica la necesidad de usar PKCE durante la autenticación para evitar la interceptación del código de autorización.
    • La propiedad GetClaimsFromUserInfoEndpoint indica que los datos del perfil de usuario deben obtenerse desde el punto final UserInfo.

Como nuestra aplicación no asume interacción con el usuario sin autenticación, vamos a garantizar que el SPA de React se cargue solo después de una autenticación exitosa. Por supuesto, si el SPA se carga de una fuente externa, como un Servidor Web Estático, por ejemplo, de los servidores de Red de Contenido Disponible (CDN) o de un servidor de desarrollo local iniciado con el comando npm start (por ejemplo, al ejecutar nuestro ejemplo en modo depuración), no será posible verificar el estado de autenticación antes de cargar el SPA. Sin embargo, cuando nuestro backend .NET propio es responsable de cargar el SPA, es posible hacerlo.

Para esto, agregue el middleware responsable de la autenticación y autorización en el archivo BffSample\Program.cs:

C#

 

app.UseRouting();
// ******************* INICIO *******************
app.UseAuthentication();
app.UseAuthorization();
// ******************** FIN ********************

Al final del archivo BffSample\Program.cs, donde se realiza directamente la transición para cargar el SPA, agregue el requisito de autorización, .RequireAuthorization():

C#

 

app.MapFallbackToFile("index.html").RequireAuthorization();

Configuración del Servidor OpenID Connect.

Como se mencionó anteriormente, para el ejemplo de integración práctica, utilizaremos un servidor de OpenID Connect de prueba basado en la biblioteca Abblix OIDC Server. La plantilla básica para una aplicación basada en ASP.NET Core MVC con la biblioteca Abblix OIDC Server también está disponible en el paquete Abblix.Templates que instalamos anteriormente. Vamos a usar esta plantilla para crear una nueva aplicación llamada OpenIDProviderApp:

Shell

 

dotnet new abblix-oidc-server -n OpenIDProviderApp

Para configurar el servidor, necesitamos registrar la aplicación BffClient como cliente en el servidor de OpenID Connect y agregar un usuario de prueba. Para esto, agregue los siguientes bloques al archivo OpenIDProviderApp\Program.cs:

C#

 

var userInfoStorage = new TestUserStorage(
    // ******************* INICIO *******************
    new UserInfo(
        Subject: "1234567890",
        Name: "John Doe",
        Email: "[email protected]",
        Password: "Jd!2024$3cur3")
    // ******************** FIN ********************
);
builder.Services.AddSingleton(userInfoStorage);

// ...

// Registrar y configurar el Servidor Abblix OIDC
builder.Services.AddOidcServices(options =>
{
    // Configurar las opciones del Servidor OIDC aquí:
    // ******************* INICIO *******************
    options.Clients = new[] {
        new ClientInfo("bff_sample") {
            ClientSecrets = new[] {
                new ClientSecret {
                    Sha512Hash = SHA512.HashData(Encoding.ASCII.GetBytes("secret")),
                }
            },
            TokenEndpointAuthMethod = ClientAuthenticationMethods.ClientSecretPost,
            AllowedGrantTypes = new[] { GrantTypes.AuthorizationCode },
            ClientType = ClientType.Confidential,
            OfflineAccessAllowed = true,
            PkceRequired = true,
            RedirectUris = new[] { new Uri("https://localhost:5003/signin-oidc", UriKind.Absolute) },
            PostLogoutRedirectUris = new[] { new Uri("https://localhost:5003/signout-callback-oidc", UriKind.Absolute) },
        }
    };
    // ******************** FIN ********************
    // La siguiente URL lleva a la acción Login del controlador AuthController
    options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);

    // La siguiente línea genera una clave nueva para la firma de tokens. Reemplaceala si desea utilizar sus propias claves.
    options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});

Vamos a revisar este código en detalle. Registramos un cliente con el identificador bff_sample y la clave secreta secret (almacenándola como una hash SHA512), indicando que la adquisición del token utilizará la autenticación del cliente con la clave secreta enviada en un mensaje POST (ClientAuthenticationMethods.ClientSecretPost). AllowedGrantTypes especifica que el cliente solo está autorizado a usar el flujo de Autorización Code. ClientType define al cliente como confidencial, lo que significa que puede almacenar su clave secreta de manera segura. OfflineAccessAllowed permite que el cliente use tokens de actualización. PkceRequired mandata el uso de PKCE durante la autenticación. RedirectUris y PostLogoutRedirectUris contienen listas de URLs permitidas para la redirección después de la autenticación y la finalización de la sesión, respectivamente.

Para cualquier otro servidor OpenID Connect, las configuraciones serán similares, con diferencias solo en cómo están configuradas.

Implementación de la API Básica BFF

Anteriormente, mencionamos que el uso del paquete Microsoft.AspNetCore.Authentication.OpenIdConnect agrega automáticamente la implementación del punto final de Iniciar Sesión a nuestra aplicación de muestra. Ahora, es momento de implementar la parte restante de la API BFF. Utilizaremos un controlador de ASP.NET MVC para estos puntos finales adicionales. Vamos a empezar agregando una carpeta Controllers y un archivo BffController.cs en el proyecto BffSample con el siguiente código dentro:

C#

 

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Mvc;

namespace BffSample.Controllers;

[ApiController]
[Route("[controller]")]
public class BffController : Controller
{
    public const string CorsPolicyName = "Bff";

    [HttpGet("check_session")]
    [EnableCors(CorsPolicyName)]
    public ActionResult> CheckSession()
    {
        // Devuelve 401 No autorizado para forzar la redirección del SPA al punto final de inicio de sesión
        if (User.Identity?.IsAuthenticated != true)
            return Unauthorized();

        return User.Claims.ToDictionary(claim => claim.Type, claim => claim.Value);
    }

    [HttpGet("login")]
    public ActionResult> Login()
    {
        // Lógica para iniciar el flujo de código de autorización
        return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
    }

    [HttpPost("logout")]
    public IActionResult Logout()
    {
        // Lógica para manejar el cierre de sesión del usuario
        return SignOut();
    }
}

Vamos a desglosar esta clase de código en detalle:

  • El atributo [Route("[controller]")] establece la ruta base para todas las acciones en el controlador. En este caso, la ruta coincidirá con el nombre del controlador, lo que significa que todas las rutas hacia nuestros métodos de la API comenzarán con /bff/.
  • La constante CorsPolicyName = "Bff" define el nombre de la política CORS (Cross-Origin Resource Sharing) que se utilizará en los atributos de método. Lo mencionaremos más adelante.
  • Los tres métodos CheckSession, Login y Logout implementan la funcionalidad necesaria del BFF descrita anteriormente. Manejan solicitudes GET en /bff/check_session y /bff/login y solicitudes POST en /bff/logout, respectivamente.
  • El método CheckSession verifica el estado de autenticación del usuario. Si el usuario no está autenticado, devuelve un código 401 No autorizado, lo que debería forzar la redirección del SPA al punto final de inicio de sesión. Si la autenticación es exitosa, el método devuelve un conjunto de reclamos y sus valores. Este método también incluye una asociación de política CORS con el nombre CorsPolicyName ya que la llamada a este método puede ser cruzada y contener cookies utilizados para la autenticación del usuario.
  • El método Login es llamado por el SPA si la llamada anterior CheckSession devolvió 401 No autorizado. Ensure que el usuario aún no está autenticado y inicia el proceso configurado Challenge, que resultará en la redirección al servidor OpenID Connect, la autenticación del usuario utilizando el flujo de código de autorización y PKCE, y la emisión de una cookie de autenticación. Después de esto, el control regresa al nodo raíz de nuestra aplicación "~/", lo que provocará que el SPA se recargue y comience con un usuario autenticado.
  • El método Logout también es llamado por el SPA, pero termina la sesión de autenticación actual. Elimina las cookies de autenticación emitidas por la parte del servidor de BffSample y también llama el punto final de sesión en el lado del servidor OpenID Connect.

Configuración de CORS para BFF

Como se mencionó anteriormente, el método CheckSession está destinado a llamadas asíncronas del SPA (normalmente utilizando la API Fetch). El funcionamiento apropiado de este método depende de la capacidad para enviar cookies de autenticación desde el navegador. Si el SPA es cargado desde un Host Web Estático separado, como una CDN o un servidor de desarrollo en un puerto separado, esta llamada se convierte en cruzada. Esto hace necesaria la configuración de una política CORS, sin la cual el SPA no podrá llamar a este método.

Ya indicamos en el código del controlador en el archivo Controllers\BffController.cs que se utilizará la política CORS denominada CorsPolicyName = "Bff". Ahora es hora de configurar los parámetros de esta política para resolver nuestro problema. Vayamos de vuelta al archivo BffSample/Program.cs y agreguemos los siguientes bloques de código:

C#

 


// ******************* EMPEZAR *******************
using BffSample.Controllers;
// ******************** TERMINAR ********************
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

// ...

builder.Services
    .AddAuthorization()
    .AddAuthentication(options => configuration.Bind("Authentication", options))
    .AddCookie()
    .AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************* EMPEZAR *******************
builder.Services.AddCors(
    options => options.AddPolicy(
        BffController.CorsPolicyName,
        policyBuilder =>
        {
            var allowedOrigins = configuration.GetSection("CorsSettings:AllowedOrigins").Get();

            if (allowedOrigins is { Length: > 0 })
                policyBuilder.WithOrigins(allowedOrigins);

            policyBuilder
                .WithMethods(HttpMethods.Get)
                .AllowCredentials();
        }));
// ******************** TERMINAR ********************
var app = builder.Build();

Este código permite que se invoquen los métodos de la política CORS desde las aplicaciones de escritorio web (SPAs) cargadas desde fuentes especificadas en la configuración como un arreglo de cadenas de texto CorsSettings:AllowedOrigins, utilizando el método GET y permite que se envíen cookies en esta llamada. Además, asegúrese de que la llamada a app.UseCors(...) se coloque justo antes de app.UseAuthentication():

C#

 

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* EMPEZAR *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** TERMINAR ********************
app.UseAuthentication();
app.UseAuthorization();

Para asegurar que la política CORS funciona correctamente, agregue el ajuste correspondiente al archivo de configuración BffSample\appsettings.Development.json:

JSON

 

{
  // ******************* EMPEZAR *******************
  "CorsSettings": {
    "AllowedOrigins": [ "https://localhost:3000" ]
  },
  // ******************** TERMINAR ********************
 "OpenIdConnect": {
   "Authority": "https://localhost:5001",
   "ClientId": "bff_sample",

En nuestro ejemplo, la dirección https://localhost:3000 es donde se inicia el servidor de desarrollo con el SPA de React utilizando el comando npm run dev. Puede encontrar esta dirección en su caso abriendo el archivo BffSample.csproj y buscando el valor del parámetro SpaProxyServerUrl. En una aplicación real, la política CORS podría incluir la dirección de su CDN (Red de Entrega de Contenidos) o un servicio similar. Es importante recordar que si su SPA se carga desde una dirección y puerto diferentes a los que proporcionan la API BFF, debe agregar esta dirección a la configuración de la política CORS.

Implementando Autenticación a través de BFF en una Aplicación React

Hemos implementado la API BFF en el lado del servidor. Ahora es el momento de centrarnos en el SPA React y agregar la funcionalidad correspondiente para llamar a esta API. Empecemos creando un archivo Bff.tsx en la carpeta BffSample\ClientApp\src\ con el siguiente contenido:

TypeScript

 

import React, { createContext, useContext, useEffect, useState, ReactNode, FC } from 'react';

// Define la estructura del contexto de BFF
interface BffContextProps {
    user: any;
    fetchBff: (endpoint: string, options?: RequestInit) => Promise;
    checkSession: () => Promise;
    login: () => void;
    logout: () => Promise;
}

// Creando un contexto para BFF para compartir estado y funciones a través de la aplicación
const BffContext = createContext({
    user: null,
    fetchBff: async () => new Response(),
    checkSession: async () => {},
    login: () => {},
    logout: async () => {}
});

interface BffProviderProps {
    baseUrl: string;
    children: ReactNode;
}

export const BffProvider: FC = ({ baseUrl, children }) => {
    const [user, setUser] = useState(null);

    // Normalizar la URL base eliminando una barra invertida en la parte final para evitar URLs inconsistentes
    if (baseUrl.endsWith('/')) {
        baseUrl = baseUrl.slice(0, -1);
    }

    const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
        try {
            // La función fetch incluye credenciales para manejar las cookies, que son necesarias para la autenticación
            return await fetch(`${baseUrl}/${endpoint}`, {
                credentials: 'include',
                ...options
            });
        } catch (error) {
            console.error(`Error during ${endpoint} call:`, error);
            throw error;
        }
    };

    // La función login redirige a la página de inicio de sesión cuando el usuario necesita autenticarse
    const login = (): void => {
        window.location.replace(`${baseUrl}/login`);
    };

    // La función checkSession es responsable de verificar la sesión del usuario al momento del renderizado inicial
    const checkSession = async (): Promise => {
        const response = await fetchBff('check_session');

        if (response.ok) {
            // Si la sesión es válida, actualizar el estado del usuario con los datos de reclamaciones recibidos
            setUser(await response.json());
        } else if (response.status === 401) {
            // Si el usuario no está autenticado, redirigirlo a la página de inicio de sesión
            login();
        } else {
            console.error('Unexpected response from checking session:', response);
        }
    };

    // Función para cerrar la sesión del usuario
    const logout = async (): Promise => {
        const response = await fetchBff('logout', { method: 'POST' });

        if (response.ok) {
            // Redirigir a la página de inicio después de cerrar correctamente la sesión
            window.location.replace('/');
        } else {
            console.error('Logout failed:', response);
        }
    };

    // useEffect se utiliza para ejecutar la función checkSession una vez que el componente se monta
    // Esto asegura que se revise la sesión inmediatamente cuando la aplicación se carga
    useEffect(() => { checkSession(); }, []);

    return (
        // Proporcionar el contexto BFF con valores y funciones relevantes para su uso a través de toda la aplicación
        
            {children}
        
    );
};

// Hook personalizado para utilizar fácilmente el contexto BFF en otros componentes
export const useBff = (): BffContextProps => useContext(BffContext);

// Exportar HOC para proporcionar acceso al Contexto BFF
export const withBff = (Component: React.ComponentType) => (props: any) =>
    
        {context => }
    ;

Este archivo exporta:

  • El componente BffProvider, que crea un contexto para BFF y proporciona funciones y estado relacionados con la autenticación y el manejo de sesiones para toda la aplicación.
  • El custom hook useBff(), que devuelve un objeto con el estado actual del usuario y funciones para trabajar con BFF: checkSession, login, y logout. Se destina para uso en componentes React funcionales.
  • El Componente de Orden Superior (HOC) withBff para usarse en componentes React basados en clases.

A continuación, creamos un componente UserClaims, que mostrará las reivindicaciones del usuario actual una vez autenticado exitosamente. Cree un archivo UserClaims.tsx en la carpeta BffSample\ClientApp\src\components con el siguiente contenido:

TypeScript

 

import React from 'react';
import { useBff } from './Bff';

export const UserClaims: React.FC = () => {
    const { user } = useBff();

    if (!user)
        return <div>Checking user session...</div>;

    return (
        <>
            <h2>User Claims</h2>
            {Object.entries(user).map(([claim, value]) => (
                <div key={claim}>
                    <strong>{claim}</strong>: {String(value)}
                </div>
            ))}
        </>
    );
};

Este código verifica si hay un usuario autenticado utilizando el custom hook useBff() y muestra las reivindicaciones del usuario como una lista si el usuario está autenticado. Si los datos del usuario todavía no están disponibles, muestra el texto Checking user session....

Ahora, vamos al archivo BffSample\ClientApp\src\App.tsx. Reemplace su contenido con el código necesario. Importe BffProvider de components/Bff.tsx y UserClaims de components/UserClaims.tsx, y inserte el código del componente principal:

TypeScript

 

import { BffProvider, useBff } from './components/Bff';
import { UserClaims } from './components/UserClaims';

const LogoutButton: React.FC = () => {
    const { logout } = useBff();
    return (
        <button className="logout-button" onClick={logout}>
            Logout
        </button>
    );
};

const App: React.FC = () => (
    <BffProvider baseUrl="https://localhost:5003/bff">
        <div className="card">
            <UserClaims/>
        </div>
        <div className="card">
            <LogoutButton />
        </div>
    </BffProvider>
);

export default App;

En este caso, el parámetro baseUrl especifica la URL base de nuestra API BFF https://localhost:5003/bff. Esta simplificación es intencional y se hace por simplicidad. En una aplicación real, debería proporcionar este ajuste de manera dinámica en lugar de codificarlo de manera estática. Existen varias maneras de lograr esto, pero discutirlas está fuera del scope de este artículo.

El botón Cerrar sesión permite al usuario cerrar sesión. Llama a la función logout disponible a través del hook useBff y redirige el navegador del usuario hacia el punto final /bff/logout, que termina la sesión del usuario en el lado del servidor.

En esta etapa, ahora puede ejecutar la aplicación BffSample junto con la aplicación OpenIDProviderApp y probar su funcionalidad. Puede usar el comando dotnet run -lp https en cada proyecto o en su IDE favorito para iniciarlos. Ambas aplicaciones deben estar ejecutándose simultáneamente.

Después de esto, abra su navegador y vaya a https://localhost:5003. Si todo está configurado correctamente, el SPA cargará y llamará a /bff/check_session. El punto final /check_session devolverá una respuesta 401, que provocará que el SPA redirija el navegador a /bff/login, que entonces iniciará la autenticación en el servidor mediante el flujo de autorización OpenID Connect usando PKCE. Puede observar esta secuencia de solicitudes abriendo la consola de desarrollo en su navegador y viniendo a la pestaña Red. Después de ingresar correctamente las credenciales del usuario ([email protected], Jd!2024$3cur3), el control regresará al SPA y verá las reclamaciones del usuario autenticado en el navegador:

Plain Text

 

sub: 1234567890
sid: V14fb1VQbAFG6JXTYQp3D3Vpa8klMLcK34RpfOvRyxQ
auth_time: 1717852776
name: John Doe
email: [email protected]

Además, haciendo clic en el botón Cerrar sesión redirigirá el navegador a /bff/logout, que cerrará la sesión del usuario y verá de nuevo la página de inicio de sesión con un prompt para ingresar su nombre de usuario y contraseña.

Si encuentras cualquier error, puedes comparar tu código con nuestro repositorio de GitHub Abblix/Oidc.Server.GettingStarted, que contiene este ejemplo y otros listos para ejecutarse.

Resolución de problemas de confianza en certificados HTTPS

Cuando estás probando localmente aplicaciones web configuradas para funcionar sobre HTTPS, puedes encontrar avisos de que el certificado SSL no es confiable. Esto ocurre porque los certificados de desarrollo usados por ASP.NET Core no son emitidos por una Autoridad de Certificación (CA) reconocida, sino que son firmados autómaticamente o simplemente no están presentes en el sistema. Estos avisos pueden eliminarse ejecutando la siguiente orden una sola vez:

Shell

 

dotnet dev-certs https --trust

Esta orden genera un certificado auto- firmado para localhost y lo instala en tu sistema para que confíe en este certificado. El certificado será usado por ASP.NET Core para ejecutar aplicaciones web localmente. Después de ejecutar esta orden, reinicia tu navegador para que las modificaciones surtan efecto.

Nota especial para usuarios de Chrome: Incluso después de instalar el certificado de desarrollo como confiable, algunas versiones de Chrome pueden restringir el acceso a sitios de localhost por motivos de seguridad. Si encuentras un error que indica que tu conexión no es segura y Chrome bloquea el acceso a localhost, puedes bypassear esto de la siguiente manera:

  • Haz clic en cualquier lugar de la página de error y escribe thisisunsafe o badidea, dependiendo de la versión de Chrome que tengas. Estas secuencias de teclas actúan como comandos de bypass en Chrome, permitiéndote proseguir hasta el sitio de localhost.

Es importante utilizar estos métodos de bypass sólo en escenarios de desarrollo, ya que pueden representar riesgos reales de seguridad.

Llamada a API de Terceros a través de BFF

Hemos implementado correctamente la autenticación en nuestra aplicación BffSample. Ahora vamos a proceder a llamar a una API de terceros que requiere un token de acceso.

Imaginemos que tenemos un servicio separado que proporciona los datos necesarios, como por ejemplo las previsiones del tiempo, y se le concede acceso solamente con un token de acceso. El papel del servidor de la parte BffSample será actuar como un proxy inverso, es decir, aceptar y autenticar la solicitud de datos del SPA, agregar el token de acceso a él, reenviar esta solicitud al servicio del tiempo y luego devolver la respuesta del servicio de vuelta al SPA.

Creando el Servicio ApiSample

Antes de demostrar la llamada a API remota a través de BFF, necesitamos crear una aplicación que sirva como este API en nuestro ejemplo.

Para crear la aplicación, usaremos una plantilla proporcionada por .NET. Navegue a la carpeta que contiene los proyectos OpenIDProviderApp y BffSample, y ejecute el siguiente comando para crear la aplicación ApiSample:

Shell

 

dotnet new webapi -n ApiSample

Esta aplicación ASP.NET Core Minimal API sirve un solo punto final con la ruta /weatherforecast que proporciona datos del tiempo en formato JSON.

Antes de todo, cambie el número de puerto asignado aleatoriamente que utiliza la aplicación ApiSample localmente a un puerto fijo, el 5004. Como se mencionó anteriormente, este paso no es obligatorio, pero simplifica nuestra configuración. Para esto, abra el archivo ApiSample\Properties\launchSettings.json, busque la configuración denominada https y cambie el valor de la propiedad applicationUrl a https://localhost:5004.

Ahora vamos a hacer que la API del clima sea accesible solo con un token de acceso. Vaya al directorio del proyecto ApiSample y agregue el paquete NuGet para la autenticación de tokens de portador JWT:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Configure el esquema de autenticación y la política de autorización llamada WeatherApi en el archivo ApiSample\Program.cs:

C#

 

// ******************* INICIO *******************
using System.Security.Claims;
// ******************** FIN ********************
var builder = WebApplication.CreateBuilder(args);

// Agregue servicios al contenedor.
// Más información sobre la configuración de Swagger/OpenAPI en https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ******************* INICIO *******************
var configuration = builder.Configuration;

builder.Services
    .AddAuthentication()
    .AddJwtBearer(options => configuration.Bind("JwtBearerAuthentication", options));

const string policyName = "WeatherApi";

builder.Services.AddAuthorization(
    options => options.AddPolicy(policyName, policy =>
    {
        policy.RequireAuthenticatedUser();
        policy.RequireAssertion(context =>
        {
            var scopeValue = context.User.FindFirstValue("scope");
            if (string.IsNullOrEmpty(scopeValue))
                return false;

            var scope = scopeValue.Split(' ', StringSplitOptions.RemoveEmptyEntries);
            return scope.Contains("weather", StringComparer.Ordinal);
        });
    }));
// ******************** FIN ********************
var app = builder.Build();

Este bloque de código establece la autenticación leyendo la configuración del archivo de aplicación, incluye autorización mediante JWT (JSON Web Tokens) y configura una política de autorización llamada WeatherApi. La política de autorización WeatherApi establece los siguientes requisitos:

  • policy.RequireAuthenticatedUser(): Garantiza que solo los usuarios autenticados pueden acceder a recursos protegidos.
  • policy.RequireAssertion(context => ...): El usuario debe tener una declaración scope que incluya el valor weather. Como la declaración scope puede contener varios valores separados por espacios según RFC 8693, el valor real de scope se divide en partes individuales, y se comprueba que el array resultante contenga el valor requerido weather.

Juntas, estas condiciones aseguran que solo usuarios autenticados con un token de acceso autorizado para el ámbito weather pueden llamar al punto final protegido por esta política.

Necesitamos aplicar esta política al punto final /weatherforecast. Agregue la llamada a RequireAuthorization() como se muestra a continuación:

C#

 

app.MapGet("/weatherforecast", () =>
{

// ...

})
.WithName("GetWeatherForecast")
// ******************* START *******************
.WithOpenApi()
.RequireAuthorization(policyName);
// ******************** END ********************

Agregue las configuraciones necesarias para el esquema de autenticación al archivo appsettings.Development.json de la aplicación ApiSample:

JSON

 

{
  // ******************* START *******************
  "JwtBearerAuthentication": {
    "Authority": "https://localhost:5001",
    "MapInboundClaims": false,
    "TokenValidationParameters": {
      "ValidTypes": [ "at+jwt" ],
      "ValidAudience": "https://localhost:5004",
      "ValidIssuer": "https://localhost:5001"
    }
  },
  // ******************** END ********************
  "Logging": {
    "LogLevel": {
      "Default": "Information",

Veamos cada configuración en detalle:

  • Authority: Esta es la URL que apunta al servidor de autorización de OpenID Connect que emite tokens JWT. El proveedor de autenticación configurado en la aplicación ApiSample usará esta URL para obtener la información necesaria para verificar tokens, como claves de firma.
  • MapInboundClaims: Esta configuración controla cómo las reclamaciones entrantes del token JWT se mapean a las reclamaciones internas en ASP.NET Core. Se establece en false, lo que significa que las reclamaciones usarán sus nombres originales del JWT.
  • TokenValidationParameters:
    • ValidTypes: Establecido en at+jwt, que según RFC 9068 2.1 indica un Token de Acceso en formato JWT.
    • ValidAudience: Especifica que la aplicación aceptará tokens emitidos para el cliente https://localhost:5004.
    • ValidIssuer: Especifica que la aplicación aceptará tokens emitidos por el servidor https://localhost:5001.

Configuración adicional de OpenIDProviderApp

La combinación del servicio de autenticación OpenIDProviderApp y la aplicación cliente BffSample funciona bien para proporcionar autenticación de usuarios. Sin embargo, para permitir llamadas a una API remota, necesitamos registrar la aplicación ApiSample como un recurso con OpenIDProviderApp. En nuestro ejemplo, utilizamos el Abblix OIDC Server, que admite RFC 8707: Indicadores de recursos para OAuth 2.0. Por lo tanto, registraremos la aplicación ApiSample como un recurso con el ámbito weather. Si estás utilizando otro servidor OpenID Connect que no soporte Indicadores de recursos, aún se recomienda registrar un ámbito único para esta API remota (como weather en nuestro ejemplo).

Añade el siguiente código al archivo OpenIDProviderApp\Program.cs:

C#

 

// Registrar y configurar el Abblix OIDC Server
builder.Services.AddOidcServices(options => {
    // ******************* INICIO *******************
    options.Resources =
    [
        new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
    ];
    // ******************** FIN ********************
    options.Clients = new[] {
        new ClientInfo("bff_sample") {

En este ejemplo, registramos la aplicación ApiSample, especificando su dirección base https://localhost:5004 como una recurso y definiendo un scope específico llamado weather. En aplicaciones reales, especialmente aquellas con API complejas que constan de muchos puntos finales, es recomendable definir scopes separados para cada punto final individual o grupo de puntos finales relacionados. Esta aproximación permite un control de acceso más preciso y ofrece flexibilidad en la gestión de los derechos de acceso. Por ejemplo, puede crear scopes distintos para diferentes operaciones, módulos de aplicación o niveles de acceso de usuario, lo que permite un control más granular sobre quién puede acceder a partes específicas de su API.

Elaboración de BffSample para Proxear Solicitudes a una API Remota

La aplicación cliente BffSample ahora necesita hacer más que solicitud un token de acceso para ApiSample. También debe manejar solicitudes de la SPA a la API remota. Esto implica agregar el token de acceso obtenido del servicio OpenIDProviderApp a estas solicitudes, reenviarlas al servidor remoto y luego devolver las respuestas del servidor de vuelta a la SPA. En esencia, BffSample necesita funcionar como un proxy inverso.

En lugar de implementar manualmente la proxying de solicitudes en nuestra aplicación cliente, utilizaremos YARP (Yet Another Reverse Proxy), un producto listo hecho desarrollado por Microsoft. YARP es un proxy inverso escrito en .NET y disponible como un paquete NuGet.

Para usar YARP en la aplicación BffSample, primero agregue el paquete NuGet:

Shell

 

dotnet add package Yarp.ReverseProxy

A continuación, agregue los siguientes namespaces al inicio del archivo BffSample\Program.cs:

C#

 

using Microsoft.AspNetCore.Authentication;
using Microsoft.IdentityModel.Protocols.OpenIdConnect;
using System.Net.Http.Headers;
using Yarp.ReverseProxy.Transforms;

Antes de la llamada var app = builder.Build();, agregue el código:

C#

 

builder.Services.AddHttpForwarder();

Y entre las llamadas a app.MapControllerRoute() y app.MapFallbackToFile():

C#

 

app.MapForwarder(
    "/bff/{**catch-all}",
    configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
    builderContext =>
    {
        // Elimina el prefijo "/bff" de la ruta de solicitud
        builderContext.AddPathRemovePrefix("/bff");

        builderContext.AddRequestTransform(async transformContext =>
        {
            // Obtiene el token de acceso recibido anteriormente durante el proceso de autenticación
            var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            
            // Añade un encabezado con el token de acceso al request del proxy
            transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        });
    }).RequireAuthorization();

Vamos a desglosar qué hace este código:

  • builder.Services.AddHttpForwarder() registra los servicios necesarios de YARP en el contenedor de DI.
  • app.MapForwarder configura el reenvío de solicitudes a otro servidor o punto final.
  • "/bff/{**catch-all}" es el patrón de ruta de path que el proxy inverso responderá. Todas las solicitudes que comienzan con /bff/ serán procesadas por YARP. {**catch-all} se utiliza para capturar todos los restos de la URL después de /bff/.
  • configuration.GetValue<string>("OpenIdConnect:Resource") utiliza la configuración de la aplicación para obtener el valor de la sección OpenIdConnect:Resource. Este valor especifica la dirección de recurso a la que se enviarán las solicitudes. En nuestro ejemplo, este valor será https://localhost:5004 – la URL base donde opera la aplicación ApiSample.
  • builderContext => ... añade las transformaciones necesarias que YARP realizará en cada solicitud entrante desde el SPA. En nuestro caso, habrá dos transformaciones:
    • builderContext.AddPathRemovePrefix("/bff") elimina el prefijo /bff del camino de la solicitud original.
    • builderContext.AddRequestTransform(async transformContext => ...) añade un encabezado HTTP de Authorization a la solicitud, que contiene el token de acceso obtenido previamente durante la autenticación. Así, las solicitudes del SPA al API remoto se autenticarán usando el token de acceso, aunque el propio SPA no tenga acceso a este token.
  • .RequireAuthorization() especifica que se requiere autorización para todas las reenviadas. Solo los usuarios autorizados podrán acceder a la ruta /bff/{**catch-all}.

Para solicitar un token de acceso para el recurso https://localhost:5004 durante la autenticación, agregue el parámetro Resource con el valor https://localhost:5004 a la configuración de OpenIdConnect en el archivo BffSample/appsettings.Development.json:

JSON

 

  "OpenIdConnect": {
    // ******************* INICIO *******************
    "Resource": "https://localhost:5004",
    // ******************** FIN ********************
    "Authority": "https://localhost:5001",
    "ClientId": "bff_sample",

También, agregue otro valor weather al array scope en el archivo BffSample/appsettings.json:

JSON

 

{
  "OpenIdConnect": {

    // ...

    // ******************* INICIO *******************
    "Scope": ["openid", "profile", "email", "weather"],
    // ******************** FIN ********************

    // ...

  }
}

Notas: En un proyecto real, es necesario monitorear la expiración del token de acceso. Cuando el token está a punto de expirar, debería solicitar uno nuevo con anticipación utilizando un token de reemplazo del servicio de autenticación o manejar un error de negación de acceso de la API remota obteniendo un nuevo token y reintentando la solicitud original. Por brevedad, deliberadamente hemos omitido este aspecto en este artículo.

Solicitud de la API de Clima a través de BFF en la Aplicación SPA

El backend ya está listo. Tenemos la aplicación ApiSample, que implementa una API con autorización basada en tokens, y la aplicación BffSample, que incluye un servidor proxy invertido integrado para proporcionar acceso seguro a esta API. El paso final es agregar la funcionalidad para solicitar esta API y mostrar los datos obtenidos dentro de la aplicación React SPA.

Agregar el archivo WeatherForecast.tsx en BffSample\ClientApp\src\components con el siguiente contenido:

TypeScript

 

import React, { useEffect, useState } from 'react';
import { useBff } from './Bff';

interface Forecast {
    date: string;
    temperatureC: number;
    temperatureF: number;
    summary: string;
}

interface State {
    forecasts: Forecast[];
    loading: boolean;
}

export const WeatherForecast: React.FC = () => {
    const { fetchBff } = useBff();
    const [state, setState] = useState<State>({ forecasts: [], loading: true });
    const { forecasts, loading } = state;

    useEffect(() => {
        fetchBff('weatherforecast')
            .then(response => response.json())
            .then(data => setState({ forecasts: data, loading: false }));
    }, [fetchBff]);

    const contents = loading
        ? <p><em>Loading...</em></p>
        : (
            <table className="table table-striped" aria-labelledby="tableLabel">
                <thead>
                <tr>
                    <th>Date</th>
                    <th>Temp. (C)</th>
                    <th>Temp. (F)</th>
                    <th>Summary</th>
                </tr>
                </thead>
                <tbody>
                {forecasts.map((forecast, index) => (
                    <tr key={index}>
                        <td>{forecast.date}</td>
                        <td align="center">{forecast.temperatureC}</td>
                        <td align="center">{forecast.temperatureF}</td>
                        <td>{forecast.summary}</td>
                    </tr>
                ))}
                </tbody>
            </table>
        );

    return (
        <div>
            <h2 id="tableLabel">Weather forecast</h2>
            <p>This component demonstrates fetching data from the server.</p>
            {contents}
        </div>
    );
};

Vamos a desglosar este código:

  • La interfaz Forecast define la estructura de los datos de la previsión del tiempo, que incluye la fecha, la temperatura en Celsius y Fahrenheit, y un resumen del clima. La interfaz State describe la estructura del estado del componente, que consiste en un arreglo de previsiones del tiempo y una bandera de carga.
  • El componente WeatherForecast recupera la función fetchBff del hook useBff y la utiliza para obtener datos del clima del servidor. El estado del componente se maneja utilizando el hook useState, inicializándose con un arreglo vacío de previsiones y una bandera de carga configurada como verdadera.
  • El hook useEffect dispara la función fetchBff cuando el componente se monta, obteniendo datos de previsión del tiempo del servidor en el punto final /bff/weatherforecast. Una vez que se recibe la respuesta del servidor y se convierte en JSON, los datos se almacenan en el estado del componente (a través de setState), y se actualiza la bandera de carga a false.
  • Depués de la bandera de carga, el componente muestra un mensaje “Cargando…” o renderiza una tabla con los datos de la previsión del tiempo. La tabla incluye columnas para la fecha, la temperatura en Celsius y Fahrenheit, y un resumen del clima para cada previsión.

Ahora, agregue el componente WeatherForecast a BffSample\ClientApp\src\App.tsx:

TypeScript

 

// ******************* INICIO *******************
import { WeatherForecast } from "./components/WeatherForecast";
// ******************** FIN ********************

// ...

    
// ******************* INICIO *******************
// ******************** FIN ********************
   

Ejecución y pruebas

Si todo ha sido hecho bien, ahora puede iniciar todos los tres proyectos. Use el comando de consola dotnet run -lp https para cada aplicación para ejecutarlas con HTTPS.

Después de lanzar todas las tres aplicaciones, abra la aplicación BffSample en su navegador (https://localhost:5003) y autentique usando las credenciales [email protected] y Jd!2024$3cur3. Después de la autenticación exitosa, debería ver la lista de reclamaciones recibidas del servidor de autenticación, como se vio anteriormente. Debajo de esto, también verá la previsión del clima.

La previsión del clima es proporcionada por la aplicación separada ApiSample, que utiliza un token de acceso emitido por el servicio de autenticación OpenIDProviderApp. Ver la previsión del clima en la ventana de la aplicación BffSample indica que nuestro SPA ha llamado correctamente al backend de BffSample, que a su vez ha actuado como proxy y ha agregado el token de acceso a la llamada a ApiSample. ApiSample ha autenticado la llamada y ha respondido con un JSON que contiene la previsión del clima.

La solución completa está disponible en GitHub.

Si encuentra cualquier problema o error al implementar los proyectos de prueba, puede recurrir a la solución completa disponible en el repositorio de GitHub. Simplemente clona el repositorio Abblix/Oidc.Server.GettingStarted para acceder a los proyectos completamente implementados descritos en este artículo. Este recurso sirve tanto como herramienta para la resolución de problemas como como punto de partida sólido para crear sus propios proyectos.

Conclusión

La evolución de protocolos de autenticación como OAuth 2.0 y OpenID Connect refleja las tendencias más amplias en seguridad web y capacidades del navegador. El alejamiento de métodos obsoletos como el Flujograma Implícito hacia enfoques más seguros, como el Flujograma de Código de Autorización con PKCE, ha mejorado significativamente la seguridad. Sin embargo, las vulnerabilidades inherentes a la operación en entornos no controlados hacen de laseguridad de los SPA modernos una tarea desafiante. Almacenar tokens exclusivamente en el backend y adoptar el patrón Backend-For-Frontend (BFF) es una estrategia efectiva para mitigar riesgos y garantizar una protección robusta de los datos de los usuarios.

Los desarrolladores deben permanecer vigilantes al addressar el panorama de amenazas en constante cambio implementando nuevos métodos de autenticación y enfoques arquitectónicos actualizados. Esta actitud proactiva es crucial para construir aplicaciones web seguras y confiables. En este artículo, exploramos y implementamos un enfoque moderno para integrar OpenID Connect, BFF y SPA usando la popular stack tecnológica .NET y React. Este enfoque puede servir como una fuerte base para sus proyectos futuros.

Mientras buscamos al futuro, la evolución continua de la seguridad web exigirá incluso mayores innovaciones en autenticación y patrones arquitectónicos. Te animamos a explorar nuestro repositorio de GitHub, aportar a la evolución de soluciones de autenticación modernas y mantenerte comprometido con los avances en curso. ¡Gracias por tu atención!

Source:
https://dzone.com/articles/modern-authentication-on-dotnet