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

As tecnologias da web continuam a avançar, assim como os métodos e protocolos desenvolvidos para assegurá-las. Os protocolos OAuth 2.0 e OpenID Connect evoluíram significativamente em resposta a ameaças de segurança emergentes e à crescente complexidade das aplicações web. Métodos de autenticação tradicionais, que eram antes eficazes, estão agora 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 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ões. Este artigo explora o padrão BFF a fundo, mostrando sua aplicação prática através de uma solução mínima implementada com .NET e React. Ao final, você terá um entendimento claro 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 olhar mais de perto nestes protocolos e no seu impacto nas aplicações web modernas.

Introduzido 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 na foundação do OAuth 2.0, o protocolo OpenID Connect (OIDC) emergiu em 2014, adicionando funcionalidades de autenticação essenciais. Ele fornece aos aplicativos clientes 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 Força de Requisição de Página (CSRF) tornaram-se mais comuns. Como os SPAs frequentemente interagem com o servidor via APIs, o armazenamento e uso seguro de tokens de acesso e atualização tornaram-se cruciais 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 aumento 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 as 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, ou em breve serão, consideradas obsoletas e frequentemente não seguras. Essa 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 mobile ou uma aplicação desktop, agora é fortemente recomendado usar o Fluxo de Autorização juntamente com a Chave de Prova para troca de Código (PKCE).

Segurança de SPAs Modernos

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

Vulnerabilidades de Código JavaScript

JavaScript é uma linguagem de programação poderosa que desempenha um papel chave em SPAs modernos de Página Única (SPAs). No entanto, suas capacidades amplas e sua prevalência representam uma potencial ameaça. SPAs modernos construídos com bibliotecas e frameworks como React, Vue ou Angular, usam uma quantidade astronômica de bibliotecas e dependências. Podemos vê-los na pasta node_modules e o número dessas dependências pode estar em centenas ou até milhares. Cada uma dessas bibliotecas pode conter vulnerabilidades de diferentes níveis de criticidade, e os desenvolvedores de SPA não têm a capacidade de verificar com detalhes o código de todas as dependências usadas. frequentemente, os desenvolvedores nem sequer acompanham a lista completa das dependências, já que elas dependem umas de outras transitariamente. Mesmo desenvolvendo seu próprio código com as melhores normas de qualidade e segurança, não se pode estar completamente certo 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 o Cross-Site Scripting (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. Isto permite que o código malicioso roubue dados da página atual, interaja com a interface da aplicação, envie pedidos para o backend, roubue 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 processador moderna para acessar dados que deveriam ser isolados. Essas vulnerabilidades são particularmente perigosas para as SPAs.

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

Vulnerabilidades Spectre permitem que atacantes roubem tokens de acesso armazenados em memória de uma aplicação JavaScript, permitindo o acesso a recursos protegidos por meio de uma impersonação da aplicação legítima. A execução especulativa também pode ser usada para roubar dados de sessão do usuário, permitindo que atacantes prosseguam seus ataques mesmo depois que a SPA for fechada.

O descobrimento de outras vulnerabilidades semelhantes ao Spectre no futuro não pode ser excluído.

O Que Fazer?

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

Em resposta às ameaças listadas, muitos especialistas apoiam a abordagem de 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 sejam 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:

O uso do 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 seu roubo devido a vulnerabilidades. A gestão de sessão e tokens é realizada no servidor, permitindo um melhor controle de segurança e uma verificação de autenticação mais confiável. 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. Vamos supor 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 cliente e servidor 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 renovaçã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 consistindo em quatro endpoints principais:

  1. Check Session: serve para verificar se existe 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. Portanto, o SPA, carregado de uma terceira fonte (por exemplo, CDN), pode verificar o status de autenticação e ou 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, redireciona o navegador para esta URL, que então forma uma requisiçã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 na autenticação bem-sucedida. Faz uma solicitação direta ao servidor OpenID Connect para trocar o Código de Autorização + o verificador de 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 por sua vez 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 apoiado pela Microsoft. Este pacote suporta tanto o Fluxo de Código de Autorização quanto o PKCE, e adiciona um ponto final com a rota relativa /signin-oidc, que já implementa a funcionalidade de ponto final de Entrar necessária (veja item 3 acima). Portanto, precisamos implementar apenas os três pontos finais restantes.

Para um exemplo prático de integração, vamos utilizar 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 públicamente disponíveis do Facebook, Google, Apple e outros que cumpram com a especificação do protocolo OpenID Connect.

Para implementar o SPA no lado do frontend, vamos usar a biblioteca React, e no lado de backend, vamos usar o .NET WebAPI. Este é um conjunto de tecnologias comum na época da redação 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 SDK .NET e o Node.js. Todos os exemplos deste artigo foram desenvolvidos e testados usando .NET 8, Node.js 22 e React 18, que eram os atuais na data da redação.

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é à versão .NET 7, o SDK oferecia um modelo embutido para uma aplicação .NET WebAPI e um React SPA. Infelizmente, este modelo foi removido na versão .NET 8. É por isso que a equipa Abblix criou o 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 pode ser instalado 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 ficheiros relacionados com o SPA estão localizados na pasta BffSample\ClientApp.

Após criar o projeto, o sistema vai solicitá-lo a executar 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 uma inicialização bem-sucedida 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á a correr localmente para 5003. Esta ação não é obrigatória, mas simplificará a configuração do servidor OpenID Connect. Para fazer isso, abra o ficheiro BffSample\Properties\launchSettings.json, encontre o perfil chamado https e mude o valor da propriedade applicationUrl para https://localhost:5003.

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

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Configure duas seqüências de autenticação chamadas Cookies e OpenIdConnect no aplicativo, lendo suas configurações da configuração do aplicativo. Para 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 brevemente revisar cada configuração e seu propósito:

  • Autenticação seção: A propriedade DefaultScheme configura autenticação por padrão usando o esquema Cookies, e o DefaultChallengeSchemecode> delega a execução da autenticação ao esquema OpenIdConnect quando o usuário não pode ser autenticado pelo esquema padrão. Assim, quando o usuário é desconhecido para 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 futuras 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 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 solicitou 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 no formulário no qual elas 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 URL 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 a interceptação 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 pressupõe interação com o usuário sem autenticação, garantiremos que o React SPA seja carregado apenas após a autenticação bem-sucedida. Claro, se o SPA for carregado de uma fonte externa, como um Hospedagem Estático da Web, por exemplo, de servidores de Entrega de Conteúdo (CDN) ou de um servidor de desenvolvimento local iniciado com o comando npm start (por exemplo, quando executarmos o 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 próprio backend .NET da nossa empresa 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 prático de integração, usaremos um servidor de OpenID Connect de teste baseado na biblioteca Abblix OIDC Server. A template básica para 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 instalámos anteriormente. Vamos usar este modelo 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 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 OIDC Server 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 URL a seguir leva à ação Login do AuthController
    options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);

    // A linha a seguir 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. 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 do 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. 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 término da sessão, respectivamente.

Para qualquer outro servidor OpenID Connect, as configurações serão semelhantes, com diferenças apenas em como elas 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 final 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 estes pontos finais adicionais. 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 redireção 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 o logout do usuário
        return SignOut();
    }
}

Vamos descompactar 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 irá corresponder 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 BFF necessária descrita acima. Eles lidam com solicitações GET em /bff/check_session, /bff/login, e solicitações 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 deveria 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 já que a chamada a este método pode ser cross-domain 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 configurado de Challenge, o qual 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. Após isso, o controle retorna à raiz de nossa aplicação "~/", o que disparará o SPA para recarregar e iniciar 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 está 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, esta chamada torna-se cross-domain. Isso faz necessária 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 desta política para resolver nossa tarefa. Vamos voltar para o 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 achar 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 do que fornecem o BFF API, você deve adicionar este endereço à configuração da política de CORS.

Implementando Autenticação via BFF em um Aplicativo React

Nós implementamos a API BFF no lado do servidor. Agora é hora de concentrarmos-nos no SPA React e adicionar a função 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 partilhar estado e funções através da aplicação
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 a URL base, removendo o traço 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 tratar de cookies, necessários para a 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 de autenticação
    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, atualizar 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, redirecionar-lhe 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 o 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 a aplicação é carregada
    useEffect(() => { checkSession(); }, []);

    return (
        // Fornecendo o contexto BFF com valores e funções relevantes para serem usados através da aplicação
        
            {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 toda a aplicação.
  • 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 é destinado para uso 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 o usuário está 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 pelo 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 essa configuração de forma dinâmica em vez de codificá-la de forma rígida. Existem várias maneiras de conseguir isso, mas discutir isso é fora do escopo deste artigo.

O botão Logout permite que o usuário saia do sistema. 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ê pode agora 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 no seu IDE favorito para iniciá-los. Ambias aplicações devem estar rodando simultaneamente.

Depois disso, 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, levando o SPA a redirecionar o navegador para /bff/login, que então iniciará a autenticação no servidor via Fluxo de Autorização de Código 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, que fará o usuário sair 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 o nosso repositório GitHub Abblix/Oidc.Server.GettingStarted, que contém este e outros exemplos prontos para executar.

Resolvendo 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 por si mesmos ou não estão presentes no sistema de todo. 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 de 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 é 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 atuam como comandos de contorno no Chrome, permitindo que você prosseguir para o site de localhost.

É importante usar estes métodos de contorno apenas 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 na implementação de autenticação em 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 dados necessários, como previsões de tempo, e que o acesso a ele é concedido apenas com um token de acesso. O papel do lado do servidor de BffSample será actuar como um proxy inverso, isto é, aceitar e autenticar a solicitação de dados do SPA, adicionar o token de acesso a ela, encaminhar essa solicitação para o serviço de tempo, e depois 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 em 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 porto atribuído aleatoriamente que a aplicação ApiSample usa localmente para um porto fixo, o 5004. Como mencionado anteriormente, esta etapa não é obrigatória, mas simplifica nossa configuração. Para isso, abra o arquivo ApiSample\Properties\launchSettings.json, encontre o perfil chamado https e altere o valor da propriedade applicationUrl para https://localhost:5004.

Agora vamos tornar a API do clima acessível apenas com um token de acesso. Navigue para 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 define 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 apenas usuários autenticados possam acessar recursos protegidos.
  • policy.RequireAssertion(context => ...): O usuário deve ter uma declaração scope que inclua o valor weather. Como a declaração scope pode conter vários valores separados por espaços de acordo com o RFC 8693, o valor real scope é dividido em partes individuais, e o array resultante é verificado para conter o valor necessário weather.

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

Nós precisamos aplicar esta 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 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 da aplicação cliente BffSample funciona bem para fornecer autenticação de usuários. No entanto, 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 suporta indicadores de recurso, ainda é recomendado registrar um escopo único para esta 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 o seu endereço base 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 que consistem em muitos pontos de extremidade, é recomendável definir escopos separados para cada ponto de extremidade individual ou grupo de pontos de extremidade relacionados. Esta abordagem permite o controle de acesso mais preciso e fornece flexibilidade na gestão 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 do 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 solicitações do SPA para a API remota. Isto envolve adicionar o token de acesso obtido do serviço OpenIDProviderApp a essas solicitações, encaminhá-las para o servidor remoto e então retornar as respostas do servidor de volta ao SPA. Essencialmente, o BffSample precisa funcionar como um proxy reverso.

Em vez de implementar manualmente o proxying de solicitações na nossa aplicação cliente, usaremos o YARP (Yet Another Reverse Proxy), um produto pronto feito pela Microsoft. O YARP é um proxy reverso 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 =>
    {
        // Remove o prefixo "/bff" do caminho de 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 descompactar o que este código faz:

  • builder.Services.AddHttpForwarder() registra os serviços necessários do YARP no container de inversão de dependências.
  • 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 do URL após /bff/.
  • configuration.GetValue<string>("OpenIdConnect:Resource") usa a configuração do aplicativo para obter o valor da seção OpenIdConnect:Resource. Este valor especifica a localização do recurso para o qual as solicitações serão encaminhadas. No nosso exemplo, este valor será https://localhost:5004 – a URL base onde o aplicativo ApiSample está em execução.
  • builderContext => ...adiciona as transformações necessárias que o YARP realizará 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 previamente obtido durante a autenticação. Assim, as solicitações do SPA para a API remota serão autenticadas usando o token de acesso, mesmo que 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 à 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 com antecedência 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 bem da brevidade, omitimos deliberadamente este aspecto neste artigo.

Solicitando a API de Clima 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. A etapa final é adicionar a funcionalidade para solicitar esta API e exibir os dados obtidos dentro da SPA React.

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 do clima, 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 do clima e um sinalizador de carregamento.
  • O componente WeatherForecast recupera a função fetchBff do hook useBff e a usa para buscar dados de clima do servidor. O estado do componente é gerenciado usando o hook useState, inicializando com um array vazio de previsões e um sinalizador de carregamento configurado como verdadeiro.
  • O hook useEffect dispara a função fetchBff quando o componente é montado, buscando dados de previsão de clima do servidor no ponto final /bff/weatherforecast. Assim 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 false.
  • Conforme o valor do sinalizador de carregamento, o componente exibe ou renderiza uma tabela com os dados de previsão do clima. 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 ********************
   

Execução e Teste

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-se 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 disso, 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 atuou como 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 recorrer à 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 nesta publicação. Este recurso serve tanto como ferramenta de solução de problemas quanto como um ponto de partida solido 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 web e capacidades do navegador. Ao mudar de métodos desatualizados, como o Fluxo Implícito, para abordagens mais seguras, como o Fluxo de Código de Autorização com PKCE, a segurança foi significativamente melhorada. No entanto, as vulnerabilidades inerentes de operar em ambientes não controlados tornam a segurança de SPAs modernos uma 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 uma proteção robusta dos dados do usuário.

Os desenvolvedores devem permanecer vigilantes em abordar o panorama de ameaças em constante mudança, implementando novos métodos de autenticação e abordagens arquiteturais atualizadas. Esta abordagem proativa é crucial para construir aplicações web seguras e confiáveis. Neste artigo, exploramos e implementamos uma abordagem moderna para integrar OpenID Connect, BFF e SPA usando a stack tecnológica popular .NET e React. Essa 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. Nós encorajamos você a explorar nosso repositório do GitHub, contribuir para o desenvolvimento de soluções de autenticação modernas e manter-se atualizado com as mudanças em curso. Obrigado por sua atenção!

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