Autenticação Moderna no .NET: OpenID Connect, BFF, SPA

Como as tecnologias da web continuam a avançar, assim fazemos os métodos e protocolos projetados para assegurá-las. Os protocolos OAuth 2.0 e OpenID Connect evoluíram significativamente em resposta a ameaças de segurança emergentes e à complexidade crescente das aplicações web. Métodos de autenticação tradicionais, que eram eficazes, agora estão se tornando obsoletos para as Aplicações de Página Única (SPAs) modernas, que enfrentam novos desafios de segurança. Neste contexto, o padrão de arquitetura Backend-For-Frontend (BFF) emergiu como uma solução recomendada para organizar as interações entre SPAs e seus sistemas backend, oferecendo uma abordagem mais segura e gerenciável para autenticação e gerenciamento de sessão. Este artigo explora o padrão BFF em profundidade, mostrando sua aplicação prática através de uma solução mínima implementada com .NET e React. Ao final, você terá uma compreensão clara de como aproveitar o padrão BFF para melhorar a segurança e a funcionalidade das suas aplicações web.

Contexto Histórico

A história de OAuth 2.0 e OpenID Connect reflete a evolução contínua das tecnologias da Internet. Vamos aproximar-nos para uma olhada mais de perto nestes protocolos e no seu impacto nas aplicações web modernas.

Iniciado em 2012, o protocolo OAuth 2.0 tornou-se um padrão amplamente adotado para autorização. Ele permite que aplicações terceiras obtenham acesso limitado a recursos do usuário sem expor as credenciais do usuário para o cliente. OAuth 2.0 suporta vários fluxos, cada um projetado para adaptar flexivelmente a vários casos de uso.

Com base no protocolo OAuth 2.0, o protocolo OpenID Connect (OIDC) emergiu em 2014, adicionando funcionalidades de autenticação essenciais. Ele fornece aos aplicativos cliente uma maneira padrão para verificar a identidade do usuário e obter informações básicas sobre ele através de um ponto de acesso padrão ou adquirindo um token de ID em formato JWT (JSON Web Token).

Evolução do Modelo de Ameaça

Com as capacidades e popularidade crescentes dos SPAs, o modelo de ameaça para SPAs também evoluiu. Vulnerabilidades como Injeção de Script em Página (XSS) e Fraude de Requisição em Site (CSRF) tornaram-se mais comuns. Como os SPAs frequentemente interagem com o servidor via APIs, armazenar e usar tokens de acesso e tokens de atualização de forma segura tornou-se crucial para a segurança.

Respondendo às demandas do tempo, os protocolos OAuth e OpenID Connect continuam a evoluir para se adaptarem a novos desafios que surgem com novas tecnologias e o crescimento do número de ameaças. Ao mesmo tempo, a constante evolução das ameaças e a melhoria das práticas de segurança significam que abordagens desatualizadas já não satisfazem os requisitos de segurança modernos. Como resultado, o protocolo OpenID Connect atual oferece uma ampla gama de funcionalidades, mas muitas delas já são consideradas obsoletas e frequentemente inseguras. Esta diversidade cria dificuldades para os desenvolvedores de SPAs em escolher a maneira mais apropriada e segura de interagir com o servidor OAuth 2.0 e OpenID Connect.

Em particular, o Fluxo Implícito agora pode ser considerado obsoleto, e para qualquer tipo de cliente, seja um SPA, uma aplicação móvel ou uma aplicação desktop, é altamente recomendado agora usar o Fluxo de Autorização junto com a Chave de Prova para troca de Código (PKCE).

Segurança de SPAs Modernos

Por que SPAs modernos ainda são considerados vulneráveis, mesmo quando usando o Fluxo de Autorização com PKCE? Existem várias respostas a esta questão.

Vulnerabilidades no Código JavaScript

JavaScript é uma linguagem de programação poderosa que desempenha um papel chave em aplicações únicas de página modernas (SPAs). No entanto, sua ampla capacidade e prevalência representam uma ameaça potencial. SPAs modernos construídos em bibliotecas e frameworks como React, Vue ou Angular, usam uma quantidade astronômica de bibliotecas e dependências. Você pode vê-los na pasta node_modules e o número de dependências pode chegar a centenas ou até milhares. Cada uma destas bibliotecas pode conter vulnerabilidades de graus de criticidade variados, e os desenvolvedores de SPA não têm a capacidade de verificar ainda o código de todas as dependências usadas. frequentemente, os desenvolvedores nem sequer acompanham a lista completa das dependências, já que elas são dependentes umas das outras. Mesmo desenvolvendo seu próprio código com os padrões de qualidade e segurança mais altos, não se pode ter certeza completa da ausência de vulnerabilidades na aplicação final.

Código JavaScript malicioso, que pode ser injetado em uma aplicação de várias maneiras, através de ataques como Injeção de Script de Sites externos (XSS) ou através do comprometimento de bibliotecas de terceiros, ganha os mesmos privilégios e nível de acesso a dados que o código legítimo da aplicação. Isso permite que o código malicioso roube dados da página atual, interaja com a interface da aplicação, envie solicitudes para o backend, roube dados do armazenamento local (localStorage, IndexedDB) e até mesmo inicie sessões de autenticação por si mesma, obtendo seus próprios tokens de acesso usando o mesmo fluxo de Autorização Code e PKCE.

Vulnerabilidade Spectre

A vulnerabilidade Spectre explora as características da arquitetura de processadores modernos para acessar dados que deveriam ser isolados. Essas vulnerabilidades são particularmente perigosas para SPAs.

Primeiro, SPAs usam intensivamente JavaScript para gerenciar o estado da aplicação e interagir com o servidor. Isso aumenta a superfície de ataque para o código JavaScript malicioso que pode explorar as vulnerabilidades do Spectre. Segundo, diferentemente das aplicações com múltiplas páginas (MPA), SPAs raramente recarregam, o que significa que a página e seu código carregado permanece ativo por muito tempo. Isto dá aos atacantes muito mais tempo para executar ataques usando o código JavaScript malicioso.

Vulnerabilidades do Spectre permitem que atacantes roube tokens de acesso armazenados no memory de uma aplicação JavaScript, permitindo o acesso a recursos protegidos por meio de uma aparência de aplicação legítimo. A execução especulativa também pode ser usada para roubar dados da sessão do usuário, permitindo que atacantes continuem seus ataques mesmo depois que a SPA é fechada.

A descoberta de outras vulnerabilidades semelhantes ao Spectre no futuro não pode ser descartada.

O Que Fazer?

Vamos resumir uma conclusão intermediária importante. Modernos SPA, dependentes de uma grande quantidade de bibliotecas JavaScript de terceiros e executados no ambiente do navegador em dispositivos do usuário, operam em um ambiente de software e hardware que os desenvolvedores não podem totalmente controlar. Portanto, devemos considerar que tais aplicações são intrinsecamente vulneráveis.

Em resposta às ameaças listadas, muitos especialistas tendem a evitar completamente armazenar tokens no navegador e projetar a aplicação de modo a que os tokens de acesso e renovação sejam obtidos e processados apenas pelo lado do servidor da aplicação, e eles nunca são passados para o lado do navegador. No contexto de um SPA com backend, isso pode ser alcançado usando o padrão Backend-For-Frontend (BFF).

O esquema de interação entre o servidor de autorização (OP), o cliente (RP) implementando o padrão BFF e uma API de terceiros (Servidor de Recursos) parece assim:

Usar o padrão BFF para proteger SPAs oferece várias vantagens. Os tokens de acesso e renovação são armazenados no lado do servidor e nunca são passados para o lado do navegador, evitando sua roubada por causa de vulnerabilidades. A gestão de sessão e tokens é tratada no servidor, permitindo melhores controles de segurança e verificações de autenticação mais confiáveis. A aplicação cliente interage com o servidor através do BFF, o que simplifica a lógica da aplicação e reduce o risco de execução de código malicioso.

Implementar o Padrão Backend-For-Frontend na Plataforma .NET

Antes de prosseguirmos com a implementação prática do BFF na plataforma .NET, vamos considerar seus componentes necessários e planejar nossas ações. Suponha que já temos um servidor OpenID Connect configurado e precisamos desenvolver um SPA que trabalhe com um backend, implementar autenticação usando OpenID Connect, e organizar a interação entre as partes do servidor e do cliente usando o padrão BFF.

De acordo com o documento OAuth 2.0 para Aplicações Baseadas em Navegador, o padrão arquitetural BFF assume que o backend age como um cliente OpenID Connect, usa o Fluxo de Código de Autorização com PKCE para autenticação, obtém e armazena tokens de acesso e atualização em seu lado, e nunca os passa para o lado do SPA no navegador. O padrão BFF também assume a presença de uma API no lado do backend composta por quatro endpoints principais:

  1. Check Session: serve para verificar uma sessão de autenticação de usuário ativa. Normalmente chamado do SPA usando uma API assíncrona (fetch) e, se bem sucedido, retorna informações sobre o usuário ativo. Assim, o SPA, carregado de uma terceira fonte (por exemplo, CDN), pode verificar o status de autenticação e continuar seu funcionamento com o usuário ou prosseguir para a autenticação usando o servidor OpenID Connect.
  2. Login: inicia o processo de autenticação no servidor OpenID Connect. Normalmente, se o SPA falhar em obter dados de usuário autenticado na etapa 1 através de Verificar Sessão, ele redireciona o navegador para esta URL, que em seguida forma uma solicitação completa para o servidor OpenID Connect e redireciona o navegador para lá.
  3. Entrar: recebe o Código de Autorização enviado pelo servidor após a etapa 2, com sucesso na autenticação. Faz uma solicitação direta ao servidor OpenID Connect para trocar o Código de Autorização + o verificador do código PKCE por Tokens de Acesso e Atualização. Inicia uma sessão autenticada no lado do cliente emitindo um cookie de autenticação para o usuário.
  4. Sair: serve para encerrar a sessão de autenticação. Normalmente, o SPA redireciona o navegador para esta URL, que em seguida forma uma solicitação para o ponto final de Sessão no servidor OpenID Connect para encerrar a sessão, além de excluir a sessão no lado do cliente e o cookie de autenticação.

Agora vamos examinar ferramentas que a plataforma .NET fornece de fábrica e olharmos para que podemos usar para implementar o padrão BFF. A plataforma .NET oferece o pacote NuGet Microsoft.AspNetCore.Authentication.OpenIdConnect, que é uma implementação pronta de um cliente OpenID Connect com suporte a Microsoft. Este pacote suporta tanto o Fluxo de Código de Autorização quanto o PKCE, e adiciona um ponto final com a seqüência de caminho relativo /signin-oidc, que já implementa a funcionalidade necessária do ponto final de Entrada de Autenticação (ver item 3 acima). Portanto, precisamos implementar somente os três pontos finais restantes.

Para um exemplo prático de integração, vamos usar um servidor de teste OpenID Connect baseado na biblioteca Abblix OIDC Server. No entanto, tudo o que é mencionado abaixo se aplica a qualquer outro servidor, incluindo servidores publicamente disponíveis da Facebook, Google, Apple e outros que cumpram com a especificação do protocolo OpenID Connect.

Para implementar o SPA no lado frontend, vamos usar a biblioteca React, e no lado backend, vamos usar o .NET WebAPI. Esse é uma das tecnologias mais comuns no momento de escrita deste artigo.

O esquema geral de componentes e sua interação parece assim:

Para trabalhar com os exemplos deste artigo, você também precisará instalar o .NET SDK e o Node.js. Todos os exemplos deste artigo foram desenvolvidos e testados usando .NET 8, Node.js 22 e React 18, que eram as versões atuais no momento da escrita.

Criando um SPA de Cliente no React com um Backend no .NET

Para criar rapidamente uma aplicação de cliente, é conveniente usar um modelo pronto. Até a versão .NET 7, o SDK oferecia um modelo integrado para uma aplicação .NET WebAPI e um React SPA. Infelizmente, este modelo foi removido na versão .NET 8. É por isso que a equipe Abblix criou seu próprio modelo, que inclui um backend .NET WebApi, um frontend SPA baseado na biblioteca React e TypeScript, construído com Vite. Este modelo está disponível publicamente como parte do pacote Abblix.Templates e você pode instalá-lo executando o seguinte comando:

Shell

 

dotnet new install Abblix.Templates

Agora podemos usar o modelo chamado abblix-react. Vamos usá-lo para criar uma nova aplicação chamada BffSample:

Shell

 

dotnet new abblix-react -n BffSample

Este comando cria uma aplicação que consiste em um backend .NET WebApi e um cliente SPA React. Os arquivos relacionados ao SPA estão localizados na pasta BffSample\ClientApp.

Após criar o projeto, o sistema pedirá que você execute um comando para instalar as dependências:

Shell

 

cmd /c "cd ClientApp && npm install"

Esta ação é necessária para instalar todas as dependências necessárias para a parte de cliente da aplicação. Para um lançamento bem-sucedido do projeto, é recomendado concordar e executar este comando digitando Y (sim).

Vamos mudar imediatamente o número da porta na qual a aplicação BffSample está sendo executada localmente para 5003. Esta ação não é obrigatória, mas simplificará a configuração posterior do servidor OpenID Connect. Para fazer isso, abra o arquivo BffSample\Properties\launchSettings.json, encontre o perfil chamado https e mude o valor da propriedade applicationUrl para https://localhost:5003.

Agora, adicione o pacote NuGet que implementa o cliente OpenID Connect navegando até a pasta BffSample e executando o seguinte comando:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Configure duas schemas de autenticação chamados Cookies e OpenIdConnect no aplicativo, lendo suas configurações da configuração do aplicativo. Para fazer isso, faça alterações no arquivo BffSample\Program.cs:

C#

 

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

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

E adicione as configurações necessárias para se conectar ao servidor OpenID Connect no arquivo BffSample\appsettings.json:

JSON

 

{
  // ******************* INICIO *******************
  "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
  },
  // ******************** FIM ********************
  "Logging": {
    "LogLevel": {
      "Default": "Information",

E no arquivo BffSample\appsettings.Development.json:

JSON

 

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

Vamos revisar brevemente cada configuração e seu propósito:

  • Autenticação seção: A propriedade DefaultScheme define a autenticação por padrão usando o esquema Cookies, e DefaultChallengeScheme delega a execução da autenticação para o esquema OpenIdConnect quando o usuário não pode ser autenticado pelo esquema padrão. Assim, quando o usuário é desconhecido para a aplicação, o servidor OpenID Connect será chamado para autenticação, e após isso, o usuário autenticado receberá um cookie de autenticação, e todas as chamadas subsequentes ao servidor serão autenticadas com ele, sem contatar o servidor OpenID Connect.
  • OpenIdConnectseção:
    • SignInScheme e SignOutScheme as propriedades especificam o esquema Cookies, que será usado para salvar as informações do usuário após o login.
    • A propriedade Authority contém a URL base do servidor OpenID Connect. ClientId e ClientSecret especificam o identificador e a chave secreta da aplicação cliente, que estão registrados no servidor OpenID Connect.
    • SaveTokens indica a necessidade de salvar os tokens recebidos como resultado da autenticação do servidor OpenID Connect.
    • Scope contém uma lista de escopos que a aplicação BffClient pede acesso. Neste caso, os escopos padrão openid (identificador do usuário), profile (perfil do usuário) e email (e-mail) são solicitados.
    • MapInboundClaims é responsável pela transformação de reivindicações entrantes do servidor OpenID Connect em reivindicações usadas na aplicação. Um valor de false significa que as reivindicações serão salvas na sessão do usuário autenticado na forma em que são recebidas do servidor OpenID Connect.
    • ResponseType com o valor code indica que o cliente usará o fluxo de código de autorização.
    • ResponseMode especifica a transmissão do código de autorização na string de consulta, que é o método padrão para o fluxo de código de autorização.
    • A propriedade UsePkce indica a necessidade de usar PKCE durante a autenticação para evitar o intercetamento do código de autorização.
    • A propriedade GetClaimsFromUserInfoEndpoint indica que os dados do perfil do usuário devem ser obtidos do ponto final UserInfo.

Como nossa aplicação não assume interação com o usuário sem autenticação, vamos garantir que o React SPA só seja carregado após a autenticação bem-sucedida. Claro, se o SPA for carregado de uma fonte externa, como um Host de Sites estáticos, por exemplo, de servidores de Rede de Entrega de Conteúdo (CDN) ou de um servidor de desenvolvimento local iniciado com o comando npm start (por exemplo, quando executamos o nosso exemplo em modo de depuração), não será possível verificar o status de autenticação antes do carregamento do SPA. Mas, quando o backend .NET próprio for responsável pelo carregamento do SPA, isso é possível.

Para isso, adicione o middleware responsável pela autenticação e autorização no arquivo BffSample\Program.cs:

C#

 

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

No final do arquivo BffSample\Program.cs, onde a transição para o carregamento do SPA é feita diretamente, adicione a exigência de autorização, .RequireAuthorization():

C#

 

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

Configuração do Servidor OpenID Connect

Como mencionado anteriormente, para o exemplo de integração prática, usaremos um servidor de OpenID Connect de teste baseado na biblioteca Abblix OIDC Server. A planteia básica de uma aplicação baseada em ASP.NET Core MVC com a biblioteca Abblix OIDC Server também está disponível no pacote Abblix.Templates que instalamos anteriormente. Vamos usar esta planteia para criar uma nova aplicação chamada OpenIDProviderApp:

Shell

 

dotnet new abblix-oidc-server -n OpenIDProviderApp

Para configurar o servidor, precisamos registrar a aplicação BffClient como um cliente no servidor OpenID Connect e adicionar um usuário de teste. Para fazer isso, adicione os seguintes blocos ao arquivo OpenIDProviderApp\Program.cs:

C#

 

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

// ...

// Registrar e configurar o Abblix OIDC Server
builder.Services.AddOidcServices(options =>
{
    // Configurar as opções do Servidor OIDC aqui:
    // ******************* 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) },
        }
    };
    // ******************** FIM ********************
    // A seguinte URL leva à ação de Login do AuthController
    options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);

    // A seguinte linha gera uma chave nova para assinatura de tokens. Substitua-a se quiser usar suas próprias chaves.
    options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});

Vamos revisar este código em detalhe. Nós registramos um cliente com o identificador bff_sample e a chave secreta secret (armazenando-a como um hash SHA512), indicando que a obtenção do token usará autenticação de cliente com a chave secreta enviada em uma mensagem POST (ClientAuthenticationMethods.ClientSecretPost). AllowedGrantTypes especifica que o cliente só é permitido usar o Fluxo de Autorização Code. ClientType define o cliente como confidencial, o que significa que ele pode armazenar sua chave secreta de forma segura. OfflineAccessAllowed permite que o cliente use tokens de atualização. PkceRequired obriga o uso de PKCE durante a autenticação. RedirectUris e PostLogoutRedirectUris contêm listas de URLs permitidas para redirecionamento após a autenticação e o encerramento da sessão, respectivamente.

Para qualquer outro servidor OpenID Connect, as configurações serão semelhantes, com diferenças apenas na forma como são configuradas.

Implementando a API Básica BFF

Anteriormente, mencionámos que o uso do pacote Microsoft.AspNetCore.Authentication.OpenIdConnect adiciona automaticamente a implementação do ponto de acesso de Entrar à nossa aplicação de exemplo. Agora, é hora de implementar a parte restante da API BFF. Nós usaremos um controlador ASP.NET MVC para esses pontos adicionais de extensão. Vamos começar adicionando uma pasta Controllers e um arquivo BffController.cs no projeto BffSample com o seguinte 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()
    {
        // Retorna 401 Não Autorizado para forçar a redirecionamento do SPA para o ponto de login
        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 o fluxo de código de autorização
        return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
    }

    [HttpPost("logout")]
    public IActionResult Logout()
    {
        // Lógica para gerenciar a saída do usuário
        return SignOut();
    }
}

Vamos analisar este código de classe em detalhe:

  • O atributo [Route("[controller]")] define a rota básica para todas as ações no controlador. Neste caso, a rota corresponde ao nome do controlador, o que significa que todas as rotas para nossos métodos da API começarão com /bff/.
  • A constante CorsPolicyName = "Bff" define o nome da política CORS (Cross-Origin Resource Sharing) para uso em atributos de método. Mais tarde referiremos-nos a ela.
  • Os três métodos CheckSession, Login, e Logout implementam a funcionalidade necessária do BFF descrita acima. Eles lidam com pedidos GET em /bff/check_session, /bff/login, e pedidos POST em /bff/logout respectivamente.
  • O método CheckSession verifica o status de autenticação do usuário. Se o usuário não estiver autenticado, ele retorna o código 401 Não Autorizado, o que deve forçar o SPA a redirecionar para o ponto de autenticação. Se a autenticação for bem-sucedida, o método retorna um conjunto de reivindicações e seus valores. Este método também inclui uma ligação de política de CORS com o nome CorsPolicyName uma vez que a chamada a este método pode ser跨域 e conter cookies usados para autenticação do usuário.
  • O método Login é chamado pelo SPA se a chamada anterior de CheckSession retornar 401 Não Autorizado. Ele garante que o usuário ainda não está autenticado e inicia o processo Challenge configurado, que resultará na redireção para o servidor OpenID Connect, a autenticação do usuário usando o Fluxo de Código de Autorização e PKCE, e a emissão de um cookie de autenticação. Depois disso, o controle retorna ao root de nossa aplicação "~/", o que fará com que o SPA recarregue e inicie com um usuário autenticado.
  • O método Logout também é chamado pelo SPA, mas termina a sessão de autenticação atual. Ele remove os cookies de autenticação emitidos pela parte do servidor de BffSample e também chama o ponto final de Fim de Sessão no lado do servidor OpenID Connect.

Configurando CORS para BFF

Como mencionado acima, o método CheckSession é destinado a chamadas assíncronas do SPA (normalmente usando a API Fetch). O funcionamento correto deste método depende da capacidade de enviar cookies de autenticação do navegador. Se o SPA for carregado de um Web Host Estático Separado, como um CDN ou um servidor de desenvolvimento rodando em uma porta separada, essa chamada torna-se cross-domain. Isto exige a configuração de uma política CORS, sem a qual o SPA não será capaz de chamar este método.

Já indicámos no código do controlador no arquivo Controllers\BffController.cs que a política CORS chamada CorsPolicyName = "Bff" deve ser usada. Agora é hora de configurar os parâmetros dessa política para resolver nossa tarefa. Vamos voltar ao arquivo BffSample/Program.cs e adicionar os seguintes blocos de código:

C#

 


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

// ...

builder.Services
    .AddAuthorization()
    .AddAuthentication(options => configuration.Bind("Authentication", options))
    .AddCookie()
    .AddOpenIdConnect(options => configuration.Bind("OpenIdConnect", options));
// ******************* INICIO *******************
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();
        }));
// ******************** FIM ********************
var app = builder.Build();

Este código permite que as políticas de CORS sejam chamadas a partir de SPAs carregados de fontes especificadas na configuração como um array de strings CorsSettings:AllowedOrigins, usando o método GET e permite que as cookies sejam enviadas nesta chamada. Adicionalmente, garante que a chamada para app.UseCors(...) esteja colocada justamente antes de app.UseAuthentication():

C#

 

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* INICIO *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** FIM ********************
app.UseAuthentication();
app.UseAuthorization();

Para garantir que a política de CORS funcione corretamente, adicione a configuração correspondente ao arquivo de configuração BffSample\appsettings.Development.json:

JSON

 

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

No nosso exemplo, o endereço https://localhost:3000 é onde o servidor de desenvolvimento com o SPA React é iniciado usando o comando npm run dev. Você pode encontrar este endereço em seu caso abrindo o arquivo BffSample.csproj e encontrando o valor do parâmetro SpaProxyServerUrl. Em um aplicativo real, a política de CORS pode incluir o endereço de seu CDN (Content Delivery Network) ou um serviço similar. Importante lembrar que se o seu SPA for carregado de um endereço e porta diferentes daquele fornecendo a API BFF, você deve adicionar este endereço à configuração da política de CORS.

Implementando Autenticação via BFF em um Aplicativo React

Nós já implementamos a API BFF no lado do servidor. Agora é hora de concentrarmos-nos no SPA React e adicionar a funcionalidade correspondente para chamar essa API. Vamos começar navegando até a pasta BffSample\ClientApp\src\, criando uma pasta components e adicionando um arquivo Bff.tsx com o seguinte conteúdo:

TypeScript

 

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

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

// Criando um contexto para BFF para compartilhar estado e funções através do aplicativo
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);

    // Normaliza a URL base removendo o traco final para evitar URLs inconsistentes
    if (baseUrl.endsWith('/')) {
        baseUrl = baseUrl.slice(0, -1);
    }

    const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
        try {
            // A função fetch inclui credenciais para lidar com cookies, necessárias para autenticação
            return await fetch(`${baseUrl}/${endpoint}`, {
                credentials: 'include',
                ...options
            });
        } catch (error) {
            console.error(`Error during ${endpoint} call:`, error);
            throw error;
        }
    };

    // A função de login redireciona para a página de login quando o usuário precisa autenticar
    const login = (): void => {
        window.location.replace(`${baseUrl}/login`);
    };

    // A função checkSession é responsável por verificar a sessão do usuário na renderização inicial
    const checkSession = async (): Promise => {
        const response = await fetchBff('check_session');

        if (response.ok) {
            // Se a sessão for válida, atualize o estado do usuário com os dados de reivindicação recebidos
            setUser(await response.json());
        } else if (response.status === 401) {
            // Se o usuário não estiver autenticado, redirecione-o para a página de login
            login();
        } else {
            console.error('Unexpected response from checking session:', response);
        }
    };

    // Função para deslogar o usuário
    const logout = async (): Promise => {
        const response = await fetchBff('logout', { method: 'POST' });

        if (response.ok) {
            // Redirecionar para a página inicial após logout bem-sucedido
            window.location.replace('/');
        } else {
            console.error('Logout failed:', response);
        }
    };

    // useEffect é usado para executar a função checkSession assim que o componente for montado
    // Isto garante que a sessão seja verificada imediatamente quando o aplicativo é carregado
    useEffect(() => { checkSession(); }, []);

    return (
        // Fornecendo o contexto BFF com valores e funções relevantes para serem usados através do aplicativo
        
            {children}
        
    );
};

// Hook personalizado para usar o contexto BFF facilmente em outros componentes
export const useBff = (): BffContextProps => useContext(BffContext);

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

Este arquivo exporta:

  • O componente BffProvider, que cria um contexto para BFF e fornece funções e estado relacionados à autenticação e gerenciamento de sessão para todo o aplicativo.
  • O gancho personalizado useBff(), que retorna um objeto com o estado atual do usuário e funções para trabalhar com BFF: checkSession, login, e logout. Ele está destinado a ser usado em componentes React funcionais.
  • O Componente de Ordem Superior (HOC) withBff para uso em componentes React baseados em classes.

A seguir, crie um componente UserClaims, que exibirá as reivindicações do usuário atual após a autenticação bem-sucedida. Crie um arquivo UserClaims.tsx na pasta BffSample\ClientApp\src\components com o seguinte conteúdo:

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 se há um usuário autenticado usando o gancho useBff() e exibe as reivindicações do usuário como uma lista se o usuário estiver autenticado. Se os dados do usuário ainda não estiverem disponíveis, exibe o texto Checking user session....

Agora, vamos mudar para o arquivo BffSample\ClientApp\src\App.tsx. Substitua seu conteúdo com o código necessário. Importe BffProvider de components/Bff.tsx e UserClaims de components/UserClaims.tsx, e insira o código do 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;

Aqui, o parâmetro baseUrl especifica a URL base do nosso BFF API https://localhost:5003/bff. Esta simplificação é intencional e feita por simplicidade. Em um aplicativo real, você deve fornecer esta configuração de forma dinâmica em vez de codificá-la à mão. Existem várias maneiras de conseguir isso, mas discutir isso é fora do escopo deste artigo.

O botão Logout permite que o usuário encerre sua sessão. Ele chama a função logout disponível através do hook useBff e redireciona o navegador do usuário para o ponto final /bff/logout, que encerra a sessão do usuário no lado do servidor.

Neste estágio, você agora pode executar a aplicação BffSample juntamente com a OpenIDProviderApp e testar sua funcionalidade. Você pode usar o comando dotnet run -lp https em cada projeto ou em sua IDE favorita para iniciá-los. Ambas as aplicações devem estar rodando simultaneamente.

Após isso, abra seu navegador e navegue até https://localhost:5003. Se tudo estiver configurado corretamente, o SPA carregará e chamará /bff/check_session. O ponto final /check_session retornará uma resposta 401, fazendo com que o SPA redirecione o navegador para /bff/login, que então iniciará a autenticação no servidor via fluxo de autorização OpenID Connect usando PKCE. Você pode observar essa sequência de pedidos abrindo a console de desenvolvimento em seu navegador e indo para a aba Rede. Depois de entrar com sucesso as credenciais do usuário ([email protected], Jd!2024$3cur3), o controle voltará para o SPA e você verá as reivindicações do usuário autenticado no navegador:

Plain Text

 

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

Ainda por cima, clicando no botão Logout o navegador será redirecionado para /bff/logout, o que encerra a sessão do usuário e você verá novamente a página de login com um prompt para entrar com o seu nome de usuário e senha.

Se você encontrar quaisquer erros, você pode comparar seu código com nosso repositório GitHub Abblix/Oidc.Server.GettingStarted, que contém este e outros exemplos prontos para executar.

resolver problemas de confiança de certificado HTTPS

Ao testar aplicações web configuradas para executar sobre HTTPS localmente, você pode encontrar avisos do navegador que o certificado SSL não é confiável. Este problema ocorre porque os certificados de desenvolvimento usados por ASP.NET Core não são emitidos por uma Autoridade de Certificação (CA) reconhecida, mas são assinados pelo próprio ou não estão presentes no sistema de todas as formas. Estes avisos podem ser eliminados executando o seguinte comando uma vez:

Shell

 

dotnet dev-certs https --trust

Este comando gera um certificado auto-assinado para localhost e o instala no seu sistema para que ele confie neste certificado. O certificado será usado por ASP.NET Core para executar aplicações web localmente. Após executar este comando, reinicie seu navegador para as mudanças terem efeito.

Nota especial para usuários do Chrome: Mesmo após instalar o certificado de desenvolvimento como confiável, algumas versões do Chrome podem ainda restringir o acesso a sites em localhost por questões de segurança. Se você encontrar um erro indicando que sua conexão não é segura e o acesso a localhost está bloqueado pelo Chrome, você pode contornar isso da seguinte forma:

  • Clique em qualquer lugar na página de erro e digite thisisunsafe ou badidea, dependendo da versão do Chrome que você está usando. Estas sequências de teclas servem como comandos de contorno no Chrome, permitindo que você continue para o site em localhost.

É importante usar estes métodos de bypass somente em cenários de desenvolvimento, já que eles podem representar riscos de segurança reais.

Chamada de APIs de Terceiros via BFF

Nós tivemos sucesso implementando autenticação na nossa aplicação BffSample. Agora vamos prosseguir para chamar uma API de terceiros que exige um token de acesso.

Imagine que temos um serviço separado que fornece os dados necessários, como previsões de tempo, e acesso a ele é concedido somente com um token de acesso. O papel do lado do servidor de BffSample será atuar como um proxy inverso, ou seja, aceitar e autenticar a requisição de dados do SPA, adicionar o token de acesso a ela, encaminhar essa requisição para o serviço de tempo, e então retornar a resposta do serviço de volta para o SPA.

Criando o Serviço ApiSample

Antes de demonstrar a chamada de API remota através do BFF, precisamos criar uma aplicação que servirá como essa API no nosso exemplo.

Para criar a aplicação, usaremos um modelo fornecido pelo .NET. Navigue para a pasta que contém os projetos OpenIDProviderApp e BffSample, e execute o seguinte comando para criar a aplicação ApiSample:

Shell

 

dotnet new webapi -n ApiSample

Esta aplicação ASP.NET Core Minimal API fornece um único ponto final com a rota /weatherforecast que fornece dados de tempo em formato JSON.

A primeira coisa a fazer é alterar o número de porta aleatoriamente atribuído que a aplicação ApiSample usa localmente para um porta fixo, o 5004. Como mencionado anteriormente, este passo não é obrigatório, mas simplifica nossa configuração. Para isso, abra o arquivo ApiSample\Properties\launchSettings.json, encontre o perfil chamado https e mude o valor da propriedade applicationUrl para https://localhost:5004.

Agora vamos tornar a API do clima acessível apenas com um token de acesso. Navegue até a pasta do projeto ApiSample e adicione o pacote NuGet para autenticação de token de portador JWT:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Configure o esquema de autenticação e a política de autorização chamada WeatherApi no arquivo ApiSample\Program.cs:

C#

 

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

// Adicione serviços ao container.
// Saiba mais sobre configurar Swagger/OpenAPI em 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);
        });
    }));
// ******************** FIM ********************
var app = builder.Build();

Este bloco de código configura a autenticação lendo a configuração do aplicativo das configurações de aplicativo, inclui autorização usando JWT (Token de Web JSON), e configura uma política de autorização chamada WeatherApi. A política de autorização WeatherApi define os seguintes requisitos:

  • policy.RequireAuthenticatedUser(): Garante que somente usuários autenticados podem acessar recursos protegidos.
  • policy.RequireAssertion(context => ...): O usuário deve ter um claim de scope que inclua o valor weather. since o claim de scope pode conter vários valores separados por espaços de acordo com o RFC 8693, o valor real de scope é dividido em partes individuais, e o array resultante é verificado para conter o valor necessário de weather.

Juntos, essas condições garantem que somente usuários autenticados com um token de acesso autorizado para o scope de weather podem chamar o ponto de extremidade protegido por esta política.

Nós precisamos aplicar essa política ao ponto de extremidade /weatherforecast. Adicione a chamada para RequireAuthorization() conforme mostrado abaixo:

C#

 

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

// ...

})
.WithName("GetWeatherForecast")
// ******************* INICIO *******************
.WithOpenApi()
.RequireAuthorization(policyName);
// ******************* FIM *******************

Adicione as configurações necessárias para o esquema de autenticação ao arquivo appsettings.Development.json da aplicação ApiSample:

JSON

 

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

Vamos examinar cada configuração em detalhe:

  • Authority: Esta é a URL que aponta para o servidor de autorização OpenID Connect que emite tokens JWT. O provedor de autenticação configurado na aplicação ApiSample usará esta URL para obter as informações necessárias para verificar tokens, como chaves de assinatura.
  • MapInboundClaims: Esta configuração controla como as reivindicações de entrada do token JWT são mapeadas para as reivindicações internas no ASP.NET Core. Ela está definida como false, o que significa que as reivindicações usarão seus nomes originais do JWT.
  • TokenValidationParameters:
    • ValidTypes: Configurado para at+jwt, o que de acordo com RFC 9068 2.1 indica um Token de Acesso no formato JWT.
    • ValidAudience: Especifica que a aplicação aceitará tokens emitidos para o cliente https://localhost:5004.
    • ValidIssuer: Especifica que a aplicação aceitará tokens emitidos pelo servidor https://localhost:5001.

Configuração adicional do OpenIDProviderApp

A combinação do serviço de autenticação OpenIDProviderApp e a aplicação cliente BffSample funciona bem para fornecer autenticação de usuário. Entretanto, para permitir chamadas a um API remoto, precisamos registrar a aplicação ApiSample como um recurso com o OpenIDProviderApp. No nosso exemplo, usamos o Abblix OIDC Server, que suporta RFC 8707: Resource Indicators for OAuth 2.0. Portanto, vamos registrar a aplicação ApiSample como um recurso com o escopo weather. Se você estiver usando outro servidor OpenID Connect que não suporte indicadores de recurso, ainda é recomendável registrar um escopo único para essa API remota (como weather no nosso exemplo).

Adicione o seguinte código ao arquivo OpenIDProviderApp\Program.cs:

C#

 

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

Neste exemplo, registramos a aplicação ApiSample, especificando sua base de endereço https://localhost:5004 como um recurso e definindo um escopo específico chamado weather. Em aplicações do mundo real, especialmente aquelas com APIs complexas compostas de muitos endpoints, é recomendável definir escopos separados para cada ponto final individual ou grupo de pontos finais relacionados. Essa abordagem permite o controle de acesso mais preciso e fornece flexibilidade na gerenciamento dos direitos de acesso. Por exemplo, você pode criar escopos distintos para diferentes operações, módulos de aplicação ou níveis de acesso de usuário, permitindo um controle mais granular sobre quem pode acessar partes específicas da sua API.

Elaboração do BffSample para Proxear Solicitações para uma API Remota

A aplicação cliente BffSample agora precisa fazer mais do que apenas solicitar um token de acesso para ApiSample. Ela também precisa lidar com solicitudes do SPA para a API remota. Isso envolve adicionar o token de acesso obtido do serviço OpenIDProviderApp a essas solicitações, encaminhá-las para o servidor remoto e, em seguida, retornar as respostas do servidor de volta para o SPA. Essencialmente, o BffSample precisa funcionar como um proxy inverso.

Em vez de implementar manualmente o encaminhamento de solicitações na nossa aplicação cliente, vamos usar o YARP (Yet Another Reverse Proxy), um produto pronto feito pela Microsoft. O YARP é um proxy inverso escrito em .NET e disponível como um pacote NuGet.

Para usar o YARP na aplicação BffSample, primeiro adicione o pacote NuGet:

Shell

 

dotnet add package Yarp.ReverseProxy

Em seguida, adicione os seguintes namespaces no início do arquivo BffSample\Program.cs:

C#

 

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

Antes da chamada var app = builder.Build();, adicione o código:

C#

 

builder.Services.AddHttpForwarder();

E entre as chamadas para app.MapControllerRoute() e app.MapFallbackToFile():

C#

 

app.MapForwarder(
    "/bff/{**catch-all}",
    configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
    builderContext =>
    {
        // Remova o prefixo "/bff" do caminho da requisição
        builderContext.AddPathRemovePrefix("/bff");

        builderContext.AddRequestTransform(async transformContext =>
        {
            // Obtenha o token de acesso recebido anteriormente durante o processo de autenticação
            var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            
            // acrescente um cabeçalho com o token de acesso à requisição de proxy
            transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        });
    }).RequireAuthorization();

Vamos desconstruir o que esse código faz:

  • builder.Services.AddHttpForwarder() registra os serviços necessários do YARP no container de DI.
  • app.MapForwarder configura o encaminhamento de requisições para outro servidor ou ponto de extremidade.
  • "/bff/{**catch-all}" é o padrão de caminho de destino para o proxy inverso. Todas as solicitações que começam com /bff/ serão processadas pelo YARP. {**catch-all} é usado para capturar todos os restantes pedaços de URL após /bff/.
  • configuration.GetValue<string>("OpenIdConnect:Resource") usa as configurações do aplicativo para obter o valor da seção OpenIdConnect:Resource. Este valor especifica a URL do recurso para o qual as solicitações serão encaminhadas. Em nosso exemplo, esse valor será https://localhost:5004 – a URL base onde o aplicativo ApiSample opera.
  • builderContext => ... adiciona as transformações necessárias que o YARP executará em cada solicitação recebida do SPA. No nosso caso, haverá duas dessas transformações:
    • builderContext.AddPathRemovePrefix("/bff") remove o prefixo /bff do caminho da solicitação original.
    • builderContext.AddRequestTransform(async transformContext => ...) adiciona um cabeçalho HTTP Authorization à solicitação, contendo o token de acesso que foi obtido anteriormente durante a autenticação. Assim, as solicitações do SPA para a API remota serão autenticadas usando o token de acesso, embora o próprio SPA não tenha acesso a esse token.
  • .RequireAuthorization() especifica que a autorização é necessária para todas as solicitações encaminhadas. Somente usuários autorizados poderão acessar a rota /bff/{**catch-all}.

Para solicitar um token de acesso para o recurso https://localhost:5004 durante a autenticação, adicione o parâmetro Resource com o valor https://localhost:5004 na configuração OpenIdConnect no arquivo BffSample/appsettings.Development.json:

JSON

 

  "OpenIdConnect": {
    // ******************* START *******************
    "Resource": "https://localhost:5004",
    // ******************** END ********************
    "Authority": "https://localhost:5001",
    "ClientId": "bff_sample",

Além disso, adicione outro valor weather ao array scope no arquivo BffSample/appsettings.json:

JSON

 

{
  "OpenIdConnect": {

    // ...

    // ******************* START *******************
    "Scope": ["openid", "profile", "email", "weather"],
    // ******************** END ********************

    // ...

  }
}

Notas: Em um projeto real, é necessário monitorar a expiração do token de acesso. Quando o token estiver prestes a expirar, você deve solicitar um novo antecipadamente usando um token de atualização do serviço de autenticação ou lidar com um erro de negação de acesso da API remota obtendo um novo token e tentando novamente a solicitação original. Para o propósito de brevidade, omitimos deliberadamente este aspecto neste artigo.

Solicitando a Weather API via BFF na Aplicação SPA

O backend está pronto agora. Temos a aplicação ApiSample, que implementa uma API com autorização baseada em token, e a aplicação BffSample, que inclui um servidor proxy reverso embutido para fornecer acesso seguro a esta API. O passo final é adicionar a funcionalidade para solicitar esta API e exibir os dados obtidos dentro do React SPA.

Adicione o arquivo WeatherForecast.tsx em BffSample\ClientApp\src\components com o seguinte conteúdo:

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 descompactar este código:

  • A interface Forecast define a estrutura dos dados de previsão de tempo, que inclui a data, a temperatura em Celsius e Fahrenheit, e um resumo do clima. A interface State descreve a estrutura do estado do componente, consistindo em um array de previsões de tempo e um sinalizador de carregamento.
  • O componente WeatherForecast recupera a função fetchBff do hook useBff e a usa para buscar dados de tempo do servidor. O estado do componente é gerenciado usando o hook useState, inicializando com um array vazio de previsões e um sinalizador de carregamento definido como verdadeiro.
  • O hook useEffect dispara a função fetchBff quando o componente é montado, buscando dados de previsão de tempo do servidor no endpoint /bff/weatherforecast. Uma vez que a resposta do servidor for recebida e convertida para JSON, os dados são armazenados no estado do componente (através de setState) e o sinalizador de carregamento é atualizado para falso.
  • Dependendo do valor do sinalizador de carregamento, o componente exibe ou renderiza uma tabela com os dados de previsão de tempo. A tabela inclui colunas para a data, temperatura em Celsius e Fahrenheit, e um resumo do clima para cada previsão.

Agora, adicione o componente WeatherForecast a BffSample\ClientApp\src\App.tsx:

TypeScript

 

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

// ...

    
// ******************* INICIO *******************
// ******************** FIM ********************
   

Executando e Testando

Se tudo foi feito corretamente, você pode agora iniciar todos os três projetos. Use o comando de console dotnet run -lp https para cada aplicação para executá-las com HTTPS.

Após iniciar todas as três aplicações, abra a aplicação BffSample no seu navegador (https://localhost:5003) e autentique usando as credenciais [email protected] e Jd!2024$3cur3. Após a autenticação bem-sucedida, você deveria ver a lista de reivindicações recebidas do servidor de autenticação, como visto anteriormente. Abaixo disto, você também verá a previsão do tempo.

A previsão do tempo é fornecida pela aplicação separada ApiSample, que usa um token de acesso emitido pelo serviço de autenticação OpenIDProviderApp. Ver a previsão do tempo na janela da aplicação BffSample indica que nossa SPA conseguiu chamar o backend de BffSample, que então fez uma Chamada de Proxy para ApiSample adicionando o token de acesso. ApiSample autenticou a chamada e respondeu com um JSON contendo a previsão do tempo.

A Solução Completa está Disponível no GitHub

Se você encontrar quaisquer problemas ou erros ao implementar os projetos de teste, você pode referir-se à solução completa disponível no repositório GitHub. Simplesmente clonar o repositório Abblix/Oidc.Server.GettingStarted para acessar os projetos totalmente implementados descritos neste artigo. Este recurso serve tanto como ferramenta de resolução de problemas quanto como um bom ponto de partida para criar seus próprios projetos.

Conclusão

A evolução dos protocolos de autenticação, como OAuth 2.0 e OpenID Connect, reflete as tendências mais amplas em segurança da web e capacidades do navegador. A mudança das antigas abordagens, como o Fluxo Implícito, para approches mais seguras, como o Fluxo de Código de Autorização com PKCE, aumentou significativamente a segurança. No entanto, as vulnerabilidades inerentes de operar em ambientes não controlados tornam a garantia de segurança em aplicações modernas de SPA um tarefa desafiadora. Armazenar tokens exclusivamente no backend e adotar o padrão Backend-For-Frontend (BFF) é uma estratégia eficaz para mitigar riscos e garantir a proteção robusta dos dados do usuário.

Os desenvolvedores devem manter-se vigilantes ao lidar com o cenário de ameaças em constante mudança, implementando novos métodos de autenticação e abordagens arquiteturais atuais. Esta abordagem proativa é crucial para construir aplicações web seguras e confiáveis. Neste artigo, exploramos e implementamos uma abordagem moderna para integração de OpenID Connect, BFF e SPA usando a stack de tecnologias popular .NET e React. Esta abordagem pode servir como uma forte fundação para seus projetos futuros.

Ao olharmos para o futuro, a evolução contínua da segurança da web exigirá ainda maior inovação em autenticação e padrões arquiteturais. Encorajamos você a explorar nosso repositório no GitHub, contribuir para o desenvolvimento de soluções de autenticação modernas e manter-se comprometido com as mudanças em andamento. Obrigado por seu interesse!

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