Modern authentication op .NET: OpenID Connect, BFF, SPA

Als webtechnologieën doorgaan met het vooruitkomen, doen ook de methodes en protocollen om ze te beveiligen dat. De protocollen OAuth 2.0 en OpenID Connect zijn significant geëvolueerd in reactie op nieuwe veiligheidsbedreigingen en de toenemende complexiteit van webapplicaties. traditionele authenticatormethodes, die eens effectief waren, worden nu voor moderne Single Page Applications (SPAs) verouderd, die nieuwe veiligheidsuitdagingen moeten bevechten. In dit kader is het Backend-For-Frontend (BFF) architecturale patroon uitgegroeid tot een aanbevolen oplossing voor het organiseren van interacties tussen SPAs en hun backendsystemen, biedende een veiliger en gemakkelijker te beheren aanpak voor authenticatie en sessiebeheer. In dit artikel wordt het BFF-patroon diepgaand uitgelegd, met behulp van een minimale oplossing die is geïmplementeerd met .NET en React. U zult aan het eind een helder begrip hebben van hoe u het BFF-patroon kunt inzetten om de veiligheid en functionaliteit van uw webapplicaties te verhogen.

Geschiedkundige Context

De geschiedenis van OAuth 2.0 en OpenID Connect reflecteert de voortdurende evolutie van internettechnologieën. Laten we deze protocollen en hun impact op moderne webapplicaties nader inpecteren.

In 2012 geïntroduceerd, is het OAuth 2.0-protocol uitgegroeid tot een breed geaccepteerd standaard voor autorisatie. Het stelt derde-partijtoepassingen in staat beperkte toegang tot gebruikersbronnen te krijgen zonder de gebruikersgegevens aan de cliënt bloot te leggen. OAuth 2.0 ondersteunt verschillende stromen, elk ontworpen om flexibel aan te passen aan diverse gebruiksgevallen.

Op de basis van OAuth 2.0 werd in 2014 het protocol OpenID Connect (OIDC) ontwikkeld, dat essentiële functies voor authenticatie toevoegde. Het biedt client-toepassingen een standaardmethode om de identiteit van de gebruiker te verifiëren en basisinformatie over hen te verkrijgen via een standaardtoegangspunt of door een ID-token in JWT (JSON Web Token) formaat te verkrijgen.

Evolutie van het Threat Model

Met de toenemende mogelijkheden en populariteit van SPAs is ook het threatenmodel voor SPAs geëvolueerd. Vulnerabiliteiten als Cross-Site Scripting (XSS) en Cross-Site Request Forgery (CSRF) zijn nu algemeen verspreid. Aangezien SPAs vaak met de server interacteren via API’s, is het voor de veiligheid erg belangrijk om toegangs- en verificatietokens veilig op te slaan en te gebruiken.

In reactie op de veranderende tijden evolueren de protocollen OAuth en OpenID Connect om aan de nieuwe uitdagingen te kunnen voldoen die ontstaan met nieuwe technologieën en de toenemende威胁. Gelijktijdig betekent de constante evolutie van de threats en de verbetering van de beveiligingspraktijken dat de verouderde aanpakken niet langer voldoen aan de moderne beveiligingsvereisten. Als resultaat biedt het huidige OpenID Connect protocol een breed scala aan mogelijkheden, maar veel daarvan worden al of zullen ze snel worden beschouwd als verouderd en vaak niet veilig. Deze diversiteit maakt het moeilijker voor SPA-ontwikkelaars om de meest geschikte en veilige manier te kiezen om te interacteren met de server voor OAuth 2.0 en OpenID Connect.

Speciaal voor de impliciete flow is het nu aan te nemen dat hij verouderd is, en voor elk type client, of het nu gaat om een SPA, een mobiele applicatie of een desktopapplicatie, wordt nu heel sterke aanbevelingen gedaan om de autorisatiesleutelflow te gebruiken samen met Proof Key for Code Exchange (PKCE). 

Beveiliging van moderne SPAs

Waarom worden moderne SPAs nog steeds als kwetsbaar beschouwd, zelfs als de autorisatiesleutelflow met PKCE wordt gebruikt? Er zijn verschillende antwoorden op deze vraag.

Vulnerabilitaten in JavaScript-code

JavaScript is een krachtige programmeertaal die een sleutelrol speelt in moderne Single Page Applications (SPAs). Het breed scala en de alomvattende aanwezigheid van deze taal vormen echter een potentiële bedreiging. Moderne SPAs, die zijn gebouwd opbouwend op bibliotheken en frameworks zoals React, Vue of Angular, gebruiken een grote hoeveelheid bibliotheken en afhankelijkheden. Die kunnen u zien in de node_modules map, en het aantal dergelijke afhankelijkheden kan zijn in de honderden of zelfs duizenden. Elke van deze bibliotheken kan kritieke mate aan vulnerabilitaten bevatten, en SPA-ontwikkelaars hebben geen mogelijkheid om de code van alle gebruikte afhankelijkheden grondig te controleren. Veelal volgen de ontwikkelaars zelfs niet de volledige lijst met afhankelijkheden, aangezien die transitief aan elkaar zijn gekoppeld. Zelfs door hun eigen code op het hoogste niveau van kwaliteit en beveiliging uit te voeren, kunnen ze zich niet volledig verzekeren van het ontbreken van vulnerabilitaten in de afgeronde applicatie.

Kwaadwillige JavaScript-code, die op verschillende manieren kan worden ingevoegd in een toepassing, bijvoorbeeld door aanvallen zoals Cross-Site Scripting (XSS) of door het compromitteren van derde-partijbibliotheken, krijgt dezelfde privileges en toegang tot gegevens als de legitieme applicatiescode. Dit maakt het kwaadwillige code mogelijk om gegevens van de huidige pagina te stelen, met de applicatieinterface te interacteren, aanvragen naar de backend te sturen, gegevens uit de lokale opslag (localStorage, IndexedDB) te stelen en zelfs aanmeldingssessies te initieren, waarmee het eigen toegangstokens krijgt door middel van hetzelfde Autorisatiecode en PKCE-protocol.

Spectre-gevaren

De Spectre-gevaren misbruiken kenmerken van de moderne processorarchitectuur om toegang te krijgen tot gegevens die verplicht moeten worden geïsoleerd. Dergelijke kwetsbaarheden zijn bijzonder gevaarlijk voor SPA’s (Single Page Applications).

Eerstens gebruiken SPA’s intensief JavaScript om de toestand van de applicatie te beheren en om met de server te communiceren. Dit vergrootte het aanvalsveld voor kwaadwillige JavaScript-code die Spectre-gevaren kan misbruiken. Tweedeens, in tegenstelling tot traditionele meerpagina-applicaties (MPAs), worden SPA’s zelden opnieuw geladen, wat betekent dat de pagina en haar geladen code langdurig actief blijven. Dit geeft aanvallers veel meer tijd om aanvallen uit te voeren met behulp van kwaadwillige JavaScript-code.

Spectre-gevaren laten aanvallers toe om toegangstokens uit de geheugen van een JavaScript-applicatie te stelen, die toegang tot beschermde resources verlenen door de legitieme applicatie te impersonificeren. Speculatieve uitvoering kan ook worden gebruikt om gebruikerssessiedata te stelen, waardoor aanvallers hun aanvallen kunnen voortzetten zelfs nadat de SPA is afgesloten.

De ontdekking van andere kwetsbaarheden die lijken op Spectre in de toekomst kan niet worden uitgesloten.

Wat moet je doen?

Laat ons een belangrijke tussenstandsconclusie samenvatten. Moderne Single Page Applications (SPAs), die afhankelijk zijn van veel derde-partij JavaScript-bibliotheken en in de browseromgeving op gebruikersapparaten draaien, werken in een software- en hardwareomgeving die ontwikkelaars niet volledig kunnen controleren. Daarom moeten we dergelijke toepassingen inherent als kwetsbaar beschouwen.

Ter reactie op de genoemde bedreigingen, gaat de meerderheid van de experts ervan uit dat het compleet vermijden van het opslaan van tokens in de browser en het ontwerpen van de toepassing zodanig dat toegangstokens en vervaldatumstokens alleen door de serverzijde van de toepassing worden verkregen en verwerkt, en ze nooit naar de browserzijde worden doorgegeven, de beste optie is. In het geval van een SPA met een backend, kan dit worden behaald door middel van het Backend-For-Frontend (BFF) architecturale patroon.

Het interactieschema tussen de autorisatieserver (OP), de cliënt (RP) die het BFF-patroon implementeert, en een derde partij API (Resource Server) ziet er als volgt uit:

Het gebruik van het BFF-patroon om SPA’s te beschermen biedt verschillende voordelen. Toegangstokens en vervaldatumstokens worden op de server bijgehouden en worden nooit naar de browser verstuurd, waardoor hun diefstal wordt voorkomen door kwetsbaarheden. Sessie- en tokenbeheer worden op de server afgehandeld, waardoor er een betere veiligheidscontrole kan plaatsvinden en de authenticatievervalificatie beter kan worden gecontroleerd. De cliënttoepassing interactieert met de server via de BFF, wat het toepassingslogica simplificeert en de risico’s van kwaadaardig code-uitvoering reduceert.

Het implementeren van het Backend-For-Frontend-patroon op de .NET-platform.

Voordat we doorgaan met de praktische implementatie van BFF op de .NET-platform, laten we even de noodzakelijke componenten overwegen en ons acties plannen. Neem aan dat we reeds een geconfigureerde OpenID Connect-server hebben en dat we een SPA moeten ontwikkelen dat werkt met een backend, authentication implementeren met OpenID Connect, en de interactie tussen de server- en clientgedeelten organiseren met behulp van het BFF-patroon.

Volgens het document OAuth 2.0 for Browser-Based Applications, bestaat het BFF-architectuurpatroon uit het feit dat de backend actief is als een OpenID Connect-client, het Autorisatiecode-flits met PKCE gebruikt voor authenticatie, toegang- en vernieuwings tokens op zijn eigen kant verkrijgt en opslaat, en deze nooit doorstuurt naar de SPA-kant in de browser. Het BFF-patroon gaat ook uit van de aanwezigheid van een API op de backend-kant bestaande uit vier hoofd-endpoints:

  1. Check Session:dient om een actieve gebruikersauthenticatiesessie te controleren. Wordt meestal vanuit de SPA met behulp van een asynchroon API (fetch) aangeroepen en, indien succesvol, geeft hij informatie weer over de actieve gebruiker. Zo kan de SPA, die van een derde bron (bijv. CDN) geladen wordt, de authenticatiesituatie controleren en ofwel zijn werkzaamheden voortzetten met de gebruiker of doorgaat met de authenticatie met de OpenID Connect-server.
  2. Login: start het authenticatieproces op de OpenID Connect server. Gewoonlijk, als de SPA het geauthenticeerde gebruikersgegevens niet aan het begin van stap 1 krijgt via Check Session, wordt de browser doorverwezen naar deze URL, die vervolgens een volledige aanvraag naar de OpenID Connect server vormt en de browser daarheen doorverwijst.
  3. Meld aan: ontvangt de Autorisatie Code die door de server wordt verzonden na stap 2 bij succesvolle authenticatie. Maakt een directe aanvraag aan de OpenID Connect server om de Autorisatie Code + PKCE code verifier te ruilen voor Toegangs- en Verlengings tokens. Start een geauthenticeerde sessie op de clientkant door een autorisatie cookie aan de gebruiker uit te voeren.
  4. Uitloggen: dient om de authenticatiesessie te beëindigen. Gewoonlijk, de SPA doorverwijst de browser naar deze URL, die in zijn eigen beurt een aanvraag naar de End Session endpoint op de OpenID Connect server maakt om de sessie te beëindigen, evenals de sessie op de clientkant en de autorisatie cookie verwijdert.

Nu laten we de tools bestuderen die de .NET-platform standaard biedt en kijken waarmee we de BFF-pattern kunnen implementeren. Het .NET-platform biedt de Microsoft.AspNetCore.Authentication.OpenIdConnect NuGet-pakket aan, dat een klaarstaande implementatie is van een OpenID Connect-client die wordt ondersteund door Microsoft. Dit pakket ondersteunt zowel de Autorisatie Code Flus als PKCE, en het voegt een endpoint toe met de relatieve pad /signin-oidc, die al de noodzakelijke Meld Aan Endpoint functionaliteit bevat (zie punt 3 hierboven). Daarom moeten we alleen de overige drie endpoints implementeren.

Voor een praktische integratievoorbeeld neemt u een test OpenID Connect-server op basis van de Abblix OIDC Server-bibliotheek. Welke server dan ook, inclusief de openbaar beschikbare servers van Facebook, Google, Apple en elke andere die aan de specificaties van het OpenID Connect-protocol voldoen, kent dezelfde toepassing.

Om de SPA op de frontendkant uit te voeren, gaat u gebruik maken van de React-bibliotheek, en op de backendkant van .NET WebAPI. Dit is een van de meest voorkomende technologystacks op het moment van het schrijven van dit artikel.

Het algemeen schema van componenten en hun interactie ziet er als volgt uit:

Om mee te werken met de voorbeelden uit dit artikel, moet u ook de .NET SDK en de Node.js installeren. Alle voorbeelden in dit artikel zijn ontwikkeld en getest met .NET 8, Node.js 22 en React 18, die actueel waren op het moment van het schrijven.

Het maken van een Client SPA op React met een backend op .NET.

Om snel een klanttoepassing te maken, is het handig om een klaar gemaakte sjabloon te gebruiken. Tot versie .NET 7 bood het SDK een ingebouwde sjabloon voor een .NET WebAPI-toepassing en een React SPA. Het spijt ons dat dit sjabloon in versie .NET 8 is verwijderd. Daarom heeft de Abblix-ploeg zijn eigen sjabloon gemaakt, die een .NET WebApi backend, een frontend SPA gebaseerd op de React-bibliotheek en TypeScript bevat, gebouwd met Vite. Dit sjabloon is openbaar beschikbaar als onderdeel van het pakket Abblix.Templates, en u kunt het installeren door de volgende opdracht uit te voeren:

Shell

 

dotnet new install Abblix.Templates

Nu kunnen we het sjabloon gebruiken met de naam abblix-react. Gebruikt het om een nieuwe toepassing te maken genaamd BffSample:

Shell

 

dotnet new abblix-react -n BffSample

Deze opdracht maakt een toepassing uit die bestaat uit een .NET WebApi backend en een React SPA-client. De bestanden die te maken hebben met de SPA zijn gelegen in de map BffSample\ClientApp.

Na het maken van het project zal het systeem u vragen om een opdracht uit te voeren om de afhankelijkheden te installeren:

Shell

 

cmd /c "cd ClientApp && npm install"

Deze actie is nodig om alle vereiste afhankelijkheden voor de client-gedeelte van de toepassing te installeren. Voor het succesvol lanceren van het project wordt aangeraden om akkoord te gaan en deze opdracht uit te voeren door Y (ja) in te typen.

Laat ons de poortnummer waarop de applicatie BffSample lokaal draait direct veranderen naar 5003. Deze actie is niet verplicht, maar zal de configuratie van het OpenID Connect-server vergemakkelijken. Om dit te doen, open het bestand BffSample\Properties\launchSettings.json, zoek het profiel met de naam https en verander de waarde van de eigenschap applicationUrl in https://localhost:5003.

Voeg vervolgens het NuGet-pakket toe dat de OpenID Connect-client implementeert, door naar de map BffSample te gaan en het volgende commando uit te voeren:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Stel twee authenticatieschema’s in de toepassing in genaamd Cookies en OpenIdConnect, die hun instellingen lezen uit de toepassingsconfiguratie. Hiertoe maak je aanpassingen aan het bestand BffSample\Program.cs:

C#

 

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

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

En voeg de noodzakelijke instellingen toe voor het verbinden met de OpenID Connect-server in het bestand BffSample\appsettings.json:

JSON

 

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

En in het bestand BffSample\appsettings.Development.json:

JSON

 

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

Laten we even elke instelling en haar doel overzien:

  • Authenticatie sectie: De DefaultScheme eigenschap stelt standaard authenticatie in gebruik met behulp van het Cookies schema, en DefaultChallengeScheme neemt de uitvoering van de authenticatie over aan het OpenIdConnect schema wanneer de gebruiker niet kan worden geauthenticateerd door het standaard schema. Hiermee wordt aangegeven dat wanneer de gebruiker onbekend is voor de applicatie, de OpenID Connect server wordt aangeroepen voor authenticatie. Na de authenticatie krijgt de geauthenticeerde gebruiker een authenticatiecookie, en alle vervolgoprocessen aan de server zullen worden geauthenticateerd met behulp van deze cookie, zonder de OpenID Connect server nogmaals te contacteren.
  • OpenIdConnectsectie:
    • SignInScheme en SignOutScheme eigenschappen specificeren het Cookies schema, wat gebruikt zal worden om de informatie van de gebruiker op te slaan nadat hij is ingelogd.
    • De Authority eigenschap bevat de basis URL van de OpenID Connect server. ClientId en ClientSecret geven de identifier en geheime sleutel van de client applicatie weer, die zijn geregistreerd op de OpenID Connect server.
    • SaveTokens geeft aan dat de ontvangen tokens moeten worden opgeslagen als gevolg van de authenticatie van de OpenID Connect server.
    • Scope bevat een lijst van scopes die de BffClient applicatie toegang tot vraagt. In dit geval worden de standaard scopes openid (gebruikers-ID), profile (gebruikersprofiel) en email (e-mail) gevraagd.
    • MapInboundClaims is verantwoordelijk voor het transformeren van inkomende claims van de OpenID Connect server in claims die in de applicatie worden gebruikt. Een waarde van false betekent dat claims in de sessie van de geauthenticeerde gebruiker worden opgeslagen in de vorm waarin ze zijn ontvangen van de OpenID Connect server.
    • ResponseType met de waarde code geeft aan dat de client de Authorization Code Flow zal gebruiken.
    • ResponseMode specificeert de overdracht van de Authorization Code via de query string, die de standaardmethode is voor de Authorization Code Flow.
    • De UsePkce eigenschap geeft aan dat PKCE moet worden gebruikt tijdens de authenticatie om het afnemen van de Authorization Code te voorkomen.
    • De GetClaimsFromUserInfoEndpoint eigenschap geeft aan dat de gebruikersprofielgegevens moeten worden gehaald van het UserInfo endpoint.

Omdat onze applicatie geen interactie met de gebruiker voor de authenticatie aanneemt, zullen we er voor zorgen dat de React SPA alleen wordt geladen nadat de authenticatie succesvol is afgerond. Natuurlijk, als de SPA vanuit een externe bron zoals een Statische Web Host wordt geladen, bijvoorbeeld vanaf Content Delivery Network (CDN) servers of een lokale ontwikkelingsserver die is opgestart met de npm start opdracht (bijvoorbeeld, wanneer onze voorbeeld applicatie in debug modus wordt uitgevoerd), is het niet mogelijk om de authenticatie status te controleren voordat de SPA wordt geladen. Maar, wanneer onze eigen .NET backend verantwoordelijk is voor het laden van de SPA, is dat mogelijk.

Hiertoe moet u de middleware toevoegen die verantwoordelijk is voor de authenticatie en autorisatie in het bestand BffSample\Program.cs:

C#

 

app.UseRouting();
// ******************* START *******************
app.UseAuthentication();
app.UseAuthorization();
// ******************** END ********************

Aan het einde van het bestand BffSample\Program.cs, waar de overgang tot het laden van de SPA direct plaatsvindt, moet u de vereiste voor autorisatie toevoegen, .RequireAuthorization():

C#

 

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

Instellen van de OpenID Connect Server

ALS gezegd werd eerder zal voor het praktische integratievoorbeeld een test OpenID Connect-server worden gebruikt die is gebaseerd op de Abblix OIDC Server-bibliotheek. Het basismodel voor een toepassing gebaseerd op ASP.NET Core MVC met de Abblix OIDC Server-bibliotheek is ook beschikbaar in het Abblix.Templates-pakket dat we eerder geïnstalleerd hadden. Gebruikmij dit model om een nieuwe toepassing te maken met de naam OpenIDProviderApp:

Shell

 

dotnet new abblix-oidc-server -n OpenIDProviderApp

Om de server te configureren, moeten we de BffClient-toepassing registreren als client bij de OpenID Connect-server en een testgebruiker toevoegen. Om dit te doen, voeg de volgende blokken toe aan het bestand OpenIDProviderApp\Program.cs:

C#

 

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

// ...

// Registreer en configureer Abblix OIDC Server
builder.Services.AddOidcServices(options =>
{
    // Configureer OIDC Server-opties hier:
    // ******************* START *******************
    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) },
        }
    };
    // ******************** END ********************
    // De volgende URL leidt naar de Login-actie van de AuthController
    options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);

    // De volgende regel genereert een nieuwe sleutel voor het ondertekenen van tokens. Wijzig deze regel indien u eigen sleutels wilt gebruiken.
    options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});

Laten we deze code in detail bekijken. We registreren een cliënt met het identifier bff_sample en de geheime sleutel secret (die we opslaan als een SHA512 hash), en geven aan dat het verkrijgen van tokens de client-authenticatie met de geheime sleutel in een POST-bericht gebruikt (ClientAuthenticationMethods.ClientSecretPost). AllowedGrantTypes specificeert dat de client alleen de Autorisatieschermstroom mag gebruiken. ClientType definieert de client als vertrouwd, wat betekent dat hij zijn geheime sleutel veilig kan opslaan. OfflineAccessAllowed staat de client toe om refresh tokens te gebruiken. PkceRequired vereist de gebruik van PKCE tijdens de authenticatie. RedirectUris en PostLogoutRedirectUris bevatten respectievelijk lijsten met toegestane URL’s voor doorverwijzing na de authenticatie en bij het beëindigen van de sessie.

Voor elke andere OpenID Connect-server zal de configuratie soortgelijk zijn, met verschillen alleen in hoe ze zijn geconfigureerd.

Implementeren van de Basic BFF API

Eerder hebben we gezegd dat het gebruik van het pakket Microsoft.AspNetCore.Authentication.OpenIdConnect automatisch de implementatie van de Sign In-eindpunt toegevoegd aan onze voorbeeldtoepassing maakt. Nu is het tijd om de resterende delen van de BFF API te implementeren. We zullen een ASP.NET MVC-controller gebruiken voor deze extra eindpunten. Beginnen we door een map Controllers toe te voegen en een bestand BffController.cs in het project BffSample aan te maken met de volgende code erbij:

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()
    {
        // Geef 401 Unauthorized terug om de SPA-omleiding naar het Login endpoint te forceren
        if (User.Identity?.IsAuthenticated != true)
            return Unauthorized();

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

    [HttpGet("login")]
    public ActionResult> Login()
    {
        // Logica om de autorisatiecodeflow te starten
        return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
    }

    [HttpPost("logout")]
    public IActionResult Logout()
    {
        // Logica om de gebruiker uit te loggen
        return SignOut();
    }
}

Laat ons deze klassecode in detail uitbreiden:

  • Het [Route("[controller]")]-attribuut stelt de basisroute in voor alle acties in de controller. In dit geval komt de route overeen met de naam van de controller, wat betekent dat alle paden naar onze API-methodes beginnen met /bff/.
  • De constante CorsPolicyName = "Bff" definieert de naam van de CORS (Cross-Origin Resource Sharing)-beleid voor gebruik in methodearmen. We zullen het later verwijzen.
  • De drie methodes CheckSession, Login en Logout implementeren de nodige BFF-functionaliteit zoals beschreven hierboven. Ze behandelen GET-verzoeken aan /bff/check_session, /bff/login en POST-verzoeken aan /bff/logout respectievelijk.
  • De methode CheckSession controleert de authenticatiestatus van de gebruiker. Als de gebruiker niet geauthenticeerd is, wordt een 401 Unauthorized-code teruggegeven, die de SPA zou moeten forceren om te worden omgeleid naar het autorisatie-eindpunt. Als de authenticatie succesvol is, wordt een set van claims en hun waarden teruggegeven. Deze methode bevat ook een CORS-beleidskoppeling met de naam CorsPolicyName, omdat de aanroep naar deze methode cross-domain kan zijn en cookies bevat die voor de gebruikersauthenticatie worden gebruikt.
  • De Login-methode wordt aangeroepen door de SPA als de vorige CheckSession– aanroep een 401 Unauthorized teruggegeven heeft. Het zorgt ervoor dat de gebruiker nog steeds niet geauthenticeerd is en start de geconfigureerde Challenge-proces, wat resulteert in een doorverwijzing naar de OpenID Connect-server, gebruikerseindigedathenticatie met behulp van de Autorisatiesleutelstroom en PKCE, en het uitgeven van een authenticatiesleutel. Na dit proces keert de controle terug naar de root van onze toepassing "~/", die de SPA zal laten herladen en starten met een geauthenticeerde gebruiker.
  • De Logout-methode wordt ook aangeroepen door de SPA, maar beëindigt de huidige authenticatiesessie. Hij verwijdert de authenticatiesleutels uitgegeven door de servergedeelte van BffSample en roept ook het Eind-sessie-eindpunt aan op de kant van de OpenID Connect-server.

CORS configureren voor BFF

Zoals eerder genoemd is de CheckSession-methode bedoeld voor asynchrone aanroepen vanuit de SPA (meestal met behulp van de Fetch API). De correcte werking van deze methode hangt af van de mogelijkheid om authenticatiesleutels vanuit de browser te versturen. Als de SPA vanuit een apart Static Web Host geladen wordt, zoals een CDN of een dev-server die op een aparte poort draait, wordt deze aanroep cross-domain. Dit maakt het configureren van een CORS-beleid noodzakelijk, zonder dit kunnen de SPA deze methode niet aanroepen.

We hebben reeds aangegeven in het controllercode in het bestand Controllers\BffController.cs dat het CORS-beleid met de naam CorsPolicyName = "Bff" moet worden gebruikt. Nu is het tijd om de parameters van dit beleid te configureren om ons taken te laten uitvoeren. Laten we naar het bestand BffSample/Program.cs teruggaan en de volgende codeblokken toevoegen:

C#

 


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

// ...

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

Deze code stelt de CORS-beleidsmethoden toe te roepen vanuit SPAs die van bronnen geladen worden die in de configuratie als een array van strings CorsSettings:AllowedOrigins gespecificeerd zijn, met behulp van de GET-methode en staat toe dat cookies meegestuurd worden in deze aanroep. Bovendien zorg er voor dat de aanroep naar app.UseCors(...) direct voor app.UseAuthentication() geplaatst wordt:

C#

 

app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseRouting();
// ******************* START *******************
app.UseCors(BffController.CorsPolicyName);
// ******************** END ********************
app.UseAuthentication();
app.UseAuthorization();

Om ervoor te zorgen dat het CORS-beleid correct werkt, voeg je de corresponderende instelling toe aan het configuratiebestand BffSample\appsettings.Development.json:

JSON

 

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

In ons voorbeeld is de adres https://localhost:3000 waar de dev-server met de React SPA wordt gestart met de opdracht npm run dev. Je kunt dit adres in uw geval vinden door het bestand BffSample.csproj te openen en de waarde van het parameter SpaProxyServerUrl te zoeken. In een echte toepassing zou het CORS-beleid misschien de adres van uw CDN (Content Delivery Network) of een soortgelijk service bevatten. Het is belangrijk te onthouden dat als uw SPA vanuit een ander adres en poort geladen wordt dan deze die de BFF-API levert, u dit adres moet toevoegen aan de CORS-beleidconfiguratie.

Authenticatie via BFF implementeren in een React-toepassing

We hebben de BFF API op de serverzijde geïmplementeerd. Nu is het tijd om ons te richten op de React SPA en de corresponderende functionaliteit toe te voegen om deze API aan te roepen. Beginnen we door naar de map BffSample\ClientApp\src\ te navigeren, een map components aan te maken en een bestand Bff.tsx toe te voegen met het volgende inhoud:

TypeScript

 

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

// Defineer de vorm van het BFF-context
interface BffContextProps {
    user: any;
    fetchBff: (endpoint: string, options?: RequestInit) => Promise;
    checkSession: () => Promise;
    login: () => void;
    logout: () => Promise;
}

// Maak een context voor BFF om de status en functies over de hele toepassing te delen
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);

    // Normaliseer de basis URL door een achterliggend slash te verwijderen om inconsistente URL's te vermijden
    if (baseUrl.endsWith('/')) {
        baseUrl = baseUrl.slice(0, -1);
    }

    const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
        try {
            // De fetch-functie bevat identiteitsgegevens om met cookies te kunnen werken, nodig voor verificatie
            return await fetch(`${baseUrl}/${endpoint}`, {
                credentials: 'include',
                ...options
            });
        } catch (error) {
            console.error(`Error during ${endpoint} call:`, error);
            throw error;
        }
    };

    // De login-functie redirectt gebruikers naar de loginpagina als ze geauthenticeerd moeten worden
    const login = (): void => {
        window.location.replace(`${baseUrl}/login`);
    };

    // De checkSession-functie is verantwoordelijk voor het verificeren van de gebruikerssessie bij het eerste weergeven
    const checkSession = async (): Promise => {
        const response = await fetchBff('check_session');

        if (response.ok) {
            // Als de sessie geldig is, update de gebruikerstaat met de ontvangen claimsgegevens
            setUser(await response.json());
        } else if (response.status === 401) {
            // Als de gebruiker niet geauthenticeerd is, redirect hem naar de loginpagina
            login();
        } else {
            console.error('Unexpected response from checking session:', response);
        }
    };

    // Functie om de gebruiker uit te loggen
    const logout = async (): Promise => {
        const response = await fetchBff('logout', { method: 'POST' });

        if (response.ok) {
            // Redirect naar de startpagina nadat de afmelding succesvol is uitgevoerd
            window.location.replace('/');
        } else {
            console.error('Logout failed:', response);
        }
    };

    // useEffect wordt gebruikt om de checkSession-functie eenmaal uit te voeren nadat het component is gemount
    // Dit zorgt ervoor dat de sessie direct wordt gecontroleerd als de app wordt geladen
    useEffect(() => { checkSession(); }, []);

    return (
        // Verschaft de BFF-context met relevante waarden en functies die over de hele toepassing worden gebruikt
        
            {children}
        
    );
};

// Aangepaste钩子 om gemakkelijk de BFF-context in andere componenten te gebruiken
export const useBff = (): BffContextProps => useContext(BffContext);

// Exporteer HOC om toegang tot BFF Context te bieden
export const withBff = (Component: React.ComponentType) => (props: any) =>
    
        {context => }
    ;

Dit bestand exporteert:

  • Het BffProvider-component, dat een context voor BFF creëert en functies en statusgegevens betreffende verificatie en sessiebeheer levert voor de gehele toepassing.
  • De aangepaste hook useBff(), die een object teruggeeft met de huidige gebruikersstatus en functies om te werken met BFF: checkSession, login en logout. Het is bedoeld voor gebruik in functionele React-componenten.
  • De Higher-Order Component (HOC) withBff voor gebruik in klassegebaseerde React-componenten.

Vervolgens maak je een UserClaims component, die de claims van de huidige gebruiker weergeeft nadat de authenticatie gelukt is. Maak een bestand UserClaims.tsx in de map BffSample\ClientApp\src\components met het volgende inhoud:

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>
            ))}
        </>
    );
};

Dit code controleert of een geauthenticeerde gebruiker bestaat met behulp van de useBff() hook en toont de claims van de gebruiker als een lijst als de gebruiker geauthenticeerd is. Als de gebruikersgegevens nog niet beschikbaar zijn, toont het de tekst Checking user session....

Nu gaan we naar het bestand BffSample\ClientApp\src\App.tsx. Vervang zijn inhoud door de nodige code. Importeer BffProvider vanuit components/Bff.tsx en UserClaims vanuit components/UserClaims.tsx, en voeg de hoofdcomponentcode toe:

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;

Hier geeft de parameter baseUrl de basis URL van onze BFF API https://localhost:5003/bff aan. Deze vereenvoudiging is bedoeld voor deze les en wordt niet gehard-codeerd. In een echte toepassing moet u deze instelling dynamisch aanleveren in plaats van het hard-coderen ervan. Er zijn verschillende manieren om dit te bereiken, maar het behandeld worden van die manieren is buiten het scope van dit artikel.

De knop Uitloggen geeft de gebruiker de mogelijkheid uit te loggen. Het roept de functie logout aan die beschikbaar is via de haak useBff en redirectt de browser van de gebruiker naar het eindpunt /bff/logout, wat de sessie van de gebruiker op de serverkant beëindigt.

Op dit moment kun je de applicatie BffSample nu samen met de applicatie OpenIDProviderApp testen en haar functionaliteit controleren. U kunt de opdracht dotnet run -lp https in elk project of uw favoriete IDE gebruiken om ze op te starten. Beide applicaties moeten gelijktijdig worden uitgevoerd.

Na dit kun je uw browser openen en naar https://localhost:5003 navigeren. Als alles correct is ingesteld, zal de SPA worden geladen en /bff/check_session aanroepen. Het eindpunt /check_session zal een reactie ter waarde van 401 teruggeven, die de SPA ertoe aanzet om de browser naar /bff/login te redirecten, wat vervolgens de autorisatie op de server op basis van OpenID Connect Authorization Code Flow met behulp van PKCE in gang zet. U kunt deze reeks aanvragen observeren door de ontwikkelaarsconsole van uw browser te openen en naar de Network-tab te gaan. Na het succesvol invoeren van de gebruikersgegevens ([email protected], Jd!2024$3cur3) keert de controle terug naar de SPA en zult u de claims van de geauthenticeerde gebruiker in de browser zien:

Plain Text

 

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

Bovendien zal het klikken op de knop Uitloggen de browser redirecten naar /bff/logout, die de gebruiker uitlogt en u zult de loginpagina weer zien met een prompt om uw gebruikersnaam en wachtwoord in te voeren.

Als je fouten tegenkomt, kun je code vergelijken met onze GitHub-repository Abblix/Oidc.Server.GettingStarted, die dit en andere voorbeelden bevat die klaar zijn om te draaien.

HTTPS-certificaatvertrouwensproblemen oplossen

Bij lokaal testen van webapplicaties die zijn geconfigureerd om te draaien via HTTPS, kun je browserwaarschuwingen tegenkomen dat het SSL-certificaat niet wordt vertrouwd. Dit probleem ontstaat omdat de ontwikkelingsificaten die door ASP.NET Core worden gebruikt niet zijn uitgegeven door een erkende Certificeringsautoriteit (CA), maar zelfondertekend zijn of helemaal niet in het systeem aanwezig zijn. Deze waarschuwingen kunnen worden geëlimineerd door het volgende commando één keer uit te voeren:

Shell

 

dotnet dev-certs https --trust

Dit commando genereert eenondertekend certificaat localhost enalleert het je systeem zodat het dit certificaat vertrouwt. Het certificaat zal door ASP.NET Core worden gebruikt om webapplicaties lokaal te draaien. Na uitvoeren van dit commando, herstart je browser om de wijzigingen door te voeren.

Speciale opmerking voor Chrome-gebruikers: Zelfs na het installeren van ontwikkelingscertificaat als vertrouwd, kunnen sommige versies van Chrome de toegang tot localhost-sites nog steeds beperken om veiligheidsredenen. Als je een fout tegenkomt die aangeeft dat je verbinding niet veilig is en de toegang tot localhost door Chrome wordt geblokkeerd, kun je dit als volgt omzeilen:

  • Klik ergens op de foutpagina en typ thisisunsafe of badidea, afhankelijk van je Chrome-versie. Deze toetsaanslagreeksen fungeren als omzeilcommando’s in Chrome, waardoor je kunt doorgaan naar de localhost-site.

Het is belangrijk deze omgevingen alleen te gebruiken in ontwikkelingsscenario’s, aangezien ze echte beveiligingsrisico’s kunnen veroorzaken.

Third-Party API’s aanroepen via BFF

We hebben succesvol authenticatie geïmplementeerd in onze BffSample applicatie. Nu gaan we verder met het aanroepen van een derde-partij API die een toegangstoken vereist.

Veronderstel dat we een aparte dienst hebben die de vereiste data verschaft, zoals weervoorspellingen, en toegang tot deze service alleen mogelijk is met een toegangstoken. De rol van de servergedeelte van BffSample zal zijn om een reverse proxy te zijn, d.w.z., de aanvraag voor data vanuit de SPA accepteren en authenticatie toepassen, het toegangstoken toevoegen, deze aanvraag doorsturen naar de weerservice, en vervolgens de reactie van de dienst terugsturen naar de SPA.

Aanmaken van de ApiSample Dienst

Voordat we de externe API aanroepen door de BFF laten zien, moeten we een applicatie aanmaken die deze API zal zijn in ons voorbeeld.

Om de applicatie te maken, zullen we een sjabloon gebruiken geleverd door .NET. Ga naar de map die de projecten OpenIDProviderApp en BffSample bevat, en voer de volgende opdracht uit om de ApiSample applicatie te maken:

Shell

 

dotnet new webapi -n ApiSample

Dit ASP.NET Core Minimal API toepassing levert een enkele endpoint op met de weg /weatherforecast die weergegevens in JSON-formaat verschaft.

Als eerste veranderd u de willekeurig toegekende poortnummer dat de applicatie ApiSample lokaal gebruikt in een vast poortnummer, 5004.ALS eerder gementioneerd, is deze stap niet verplicht, maar simplificeert ons setup. Om dit te doen, opent u het bestand ApiSample\Properties\launchSettings.json, zoekt u het profiel genaamd https en veranderd u de waarde van de eigenschap applicationUrl in https://localhost:5004.

Nu zal de weer API alleen toegankelijk zijn met een toegangstoken. Ga naar de map van het project ApiSample en voeg het NuGet-pakket voor JWT Bearer token authenticatie toe:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Configureer het authenticatieschema en de autorisatiebeleid genaamd WeatherApi in het bestand ApiSample\Program.cs:

C#

 

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

// Voeg services toe aan het container.
// Leer meer over het configureren van Swagger/OpenAPI op https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// ******************* START *******************
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);
        });
    }));
// ******************** END ********************
var app = builder.Build();

Dit codeblok configureert de authenticatie door de configuratie uit de toepassingsinstellingen te lezen, omvat autorisatie met behulp van JWT (JSON Web Tokens) en configureert een autorisatiebeleid genaamd WeatherApi. Het autorisatiebeleid WeatherApi stelt de volgende vereisten:

  • policy.RequireAuthenticatedUser(): Zorgt ervoor dat alleen geauthenticeerde gebruikers toegang krijgen tot beschermde resources.
  • policy.RequireAssertion(context => ...): De gebruiker moet een scope claim hebben die de waarde weather bevat. Aangezien de scope claim meerdere waarden kan bevatten, gescheiden door spaties volgens RFC 8693, wordt de ware scope waarde in individuele delen verdeeld, en wordt de resulterende array gecontroleerd om de vereiste weather waarde te bevatten.

Met deze voorwaarden samen zorgt dit ervoor dat alleen geauthenticeerde gebruikers met een toegangstoken gemachtigd zijn voor de weather scope deze policy kunnen aanroepen.

We moeten deze policy toepassen op de /weatherforecast endpoint. Voeg de aanroep tot RequireAuthorization() toe zoals getoond onderstaand:

C#

 

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

// ...

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

Voeg de noodzakelijke configuratieinstellingen toe voor het authenticatieschema in het appsettings.Development.json bestand van de ApiSample applicatie:

JSON

 

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

Laten we elke instelling in detail bekijken:

  • Authority: Dit is de URL die naar de OpenID Connect-autorisatieserver verwijst die JWT-tokens uitgeeft. Het authenticatieprovider die in de ApiSample applicatie is geconfigureerd zal deze URL gebruiken om de informatie te verkrijgen die nodig is om tokens te verifiëren, zoals ondertekeningssleutels.
  • MapInboundClaims: Deze instelling bepaalt hoe binnenkomende claims van het JWT-token worden toegewezen aan interne claims in ASP.NET Core. Het is ingesteld op false, wat betekent dat claims hun oorspronkelijke namen van het JWT behouden.
  • TokenValidationParameters:
    • ValidTypes: Ingesteld op at+jwt, wat volgens RFC 9068 2.1 aangeeft dat het een Toegangstoken in JWT-formaat is.
    • ValidAudience: Geeft aan dat de applicatie tokens accepteert die zijn uitgegeven voor de client https://localhost:5004.
    • ValidIssuer: Geeft aan dat de applicatie tokens accepteert die zijn uitgegeven door de server https://localhost:5001.

Aanvullende configuratie van OpenIDProviderApp

De combinatie van de authenticatieservice OpenIDProviderApp en de clientapplicatie BffSample werkt goed voor het leveren van gebruikersauthenticatie. Om echter aanroepen naar een externe API mogelijk te maken, moeten we de ApiSample-applicatie registreren als een resource bij OpenIDProviderApp. In ons voorbeeld gebruiken we de Abblix OIDC Server, die RFC 8707: Resource Indicators for OAuth 2.0 ondersteund. Daarom zullen we de ApiSample-applicatie registreren als resource met het bereik weather. Als u een andere OpenID Connect-server gebruikt die geen Resource Indicators ondersteunt, wordt nog steeds aangeraden om een uniek bereik voor deze externe API te registreren (bijvoorbeeld weather in ons voorbeeld).

Voeg het volgende codefragment toe aan het bestand OpenIDProviderApp\Program.cs:

C#

 

// Register en configureer de Abblix OIDC Server
builder.Services.AddOidcServices(options => {
    // ******************* START *******************
    options.Resources =
    [
        new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
    ];
    // ******************** EINDE ********************
    options.Clients = new[] {
        new ClientInfo("bff_sample") {

In dit voorbeeld registreren we de applicatie ApiSample, door haar basisadres https://localhost:5004 als een resource op te geven en een specifieke scope naam weather definiëren. Voor echte wereldtoepassingen, vooral die met complexe API’s bestaande uit vele endpoints, is het aanbevolen om aparte scopes voor elke individuele endpoint of groep gerelateerde endpoints te definiëren. Deze aanpak biedt meer exacte toegangsbeheer en geeft flexibiliteit in het beheren van toegangsrechten. Bijvoorbeeld, u kunt aparte scopes maken voor verschillende bewerkingen, toepassingsmodule of gebruikerstoegangsniveaus, wat betekent dat u meer gedeeltelijke controle heeft over wie specifieke delen van uw API kan benaderen.

Verdieping van BffSample voor het proxifyen van verzoeken naar een externe API

De clientapplicatie BffSample moet nu meer doen dan net een toegangs token voor ApiSample aanvragen. Hij moet ook de verzoeken van de SPA naar de externe API afhandelen. Dit betekent dat we de behaalde toegangs token van de service OpenIDProviderApp aan deze verzoeken moeten toevoegen, ze doorsturen naar de externe server en vervolgens de serverreacties terugsturen naar de SPA. In essentie moet BffSample functioneren als een omgekeerd proxy-server.

In plaats van handmatig de verzoek proxying te implementeren in onze client applicatie, zullen we YARP (Yet Another Reverse Proxy) gebruiken, een klaar gemaakte product ontwikkeld door Microsoft. YARP is een omgekeerd proxy-server geschreven in .NET en beschikbaar als een NuGet-pakket.

Om YARP in de applicatie BffSample te gebruiken, moet u eerst het NuGet-pakket toevoegen:

Shell

 

dotnet add package Yarp.ReverseProxy

Voeg vervolgens de volgende namespaces aan het begin van het bestand BffSample\Program.cs toe:

C#

 

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

Voordat de aanroep var app = builder.Build(); wordt uitgevoerd, voeg de code toe:

C#

 

builder.Services.AddHttpForwarder();

En tussen de aanroepen van app.MapControllerRoute() en app.MapFallbackToFile():

C#

 

app.MapForwarder(
    "/bff/{**catch-all}",
    configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
    builderContext =>
    {
        // Verwijder het voorvoegsel "/bff" van de aanvraagpad
        builderContext.AddPathRemovePrefix("/bff");

        builderContext.AddRequestTransform(async transformContext =>
        {
            // Haal het toegangstoken op dat eerder tijdens het authenticatiesproces werd ontvangen
            var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            
            // Voeg een koptekst toe met het toegangstoken aan de proxyaanvraag
            transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        });
    }).RequireAuthorization();

Laat ons hetgedaan delen wat deze code doet:

  • builder.Services.AddHttpForwarder() registreert de nodige YARP services in het DI-container.
  • app.MapForwarder configureert de aanvraag vooruitsturen naar een andere server of endpoint.
  • "/bff/{**catch-all}" is het padpatroon waarop de omgekeerde proxy reageert. Alle aanvragen die beginnen met /bff/ zullen worden behandeld door YARP. {**catch-all} wordt gebruikt om alle resterende delen van de URL na /bff/ op te slaan.
  • configuration.GetValue<string>("OpenIdConnect:Resource") gebruikt de applicatieconfiguratie om de waarde uit de OpenIdConnect:Resource sectie te krijgen. Deze waarde specificeert de resourceadres waarnaar de aanvragen worden doorgestuurd. In ons voorbeeld is deze waarde https://localhost:5004 – de basisURL waar de applicatie ApiSample operationeert.
  • builderContext => ...voegt de nodige transformaties toe die YARP zal uitvoeren op elk inkomend verzoek van de SPA. In ons geval zullen er twee van dergelijke transformaties zijn:
    • builderContext.AddPathRemovePrefix("/bff") verwijdert het /bff voorvoegsel van het oorspronkelijke verzoekpad.
    • builderContext.AddRequestTransform(async transformContext => ...) voegt een Authorization HTTP-header toe aan het verzoek, met het toegangstoken dat eerder tijdens de authenticatie is verkregen. Hierdoor zullen verzoeken van de SPA naar de externe API worden geauthenticeerd met behulp van het toegangstoken, ook al heeft de SPA zelf geen toegang tot dit token.
  • .RequireAuthorization() specificeert dat autorisatie vereist is voor alle doorgezonden verzoeken. Alleen geautoriseerde gebruikers zullen toegang hebben tot de route /bff/{**catch-all}.

Om een toegangstoken te verkrijgen voor de resource `https://localhost:5004` tijdens de authenticatie, moet u het parameter `Resource` toevoegen met de waarde `https://localhost:5004` aan de `OpenIdConnect` configuratie in het bestand `BffSample/appsettings.Development.json`:`

JSON

 

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

`Ook moet u een andere waarde `weather` toevoegen aan de array `scope` in het bestand `BffSample/appsettings.json`:`

JSON

 

{
  "OpenIdConnect": {

    // ...

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

    // ...

  }
}

Notities: In een echte project is het nodig om de vervaldatum van het toegangstoken te monitoren. Als het token binnenkort verloopt, moet u er voor zorgen dat een nieuw wordt aangevraagd met behulp van een vernieuwingstoken van de authenticatieservice of door een toegangsexclusiefout van de externe API af te handelen door een nieuw token te verkrijgen en de originele aanvraag opnieuw te proberen. Om redelijkheid te behouden, hebben we dit aspect opzettelijk in dit artikel overgeslagen.`

Weather API aanvragen via BFF in de SPA-toepassing

Het backend is nu klaar. We hebben de `ApiSample`-toepassing, die een API met tokengebaseerde autorisatie implementeert, en de `BffSample`-toepassing, die een ingebouwd omgekeerd proxy-server bevat om toegang tot deze API veilig te bieden. Het laatste stap is het toevoegen van de functionaliteit om deze API aan te vragen en de verkregen gegevens binnen de React SPA weer te geven.

Voeg het bestand WeatherForecast.tsx toe in BffSample\ClientApp\src\components met het volgende inhoud:

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>
    );
};

Laat’s de code opbreken:

  • Het Forecast-interface definieert de structuur van de weersvoorspellinggegevens, die bestaat uit de datum, temperatuur in Celsius en Fahrenheit, en een samenvatting van het weer. Het State-interface beschrijft de structuur van de componentestate, bestaande uit een array van weersvoorspellingen en een laadvlag.
  • De WeatherForecast-component haalt de fetchBff-functie van het useBff-haksel uit en gebruikt deze om weergegevens van de server op te halen. De componentestate wordt beheerd met behulp van het useState-haksel, met een lege array van vooruitzichten en een laadvlag ingesteld op waar.
  • Het useEffect-haksel activeert de fetchBff-functie als de component wordt gemonteerd, en haalt weersvoorspellinggegevens van de server op bij de endpoint /bff/weatherforecast. Zodra de serverreactie is ontvangen en naar JSON is geconverteerd, worden de gegevens in de componentestate opgeslagen (via setState) en wordt de laadvlag bijgewerkt naar false.
  • Afhankelijk van de waarde van de laadvlag, toont de component ofwel een “Loading…”-bericht of rendeert een tabel met de weersvoorspellingengegevens. De tabel bevat kolommen voor de datum, temperatuur in Celsius en Fahrenheit, en een samenvatting van het weer voor elke voorspelling.

Nu voeg je de WeatherForecast-component toe aan BffSample\ClientApp\src\App.tsx:

TypeScript

 

// ******************* START *******************
import { WeatherForecast } from "./components/WeatherForecast";
// ******************** EINDE ********************

// ...

    
// ******************* START *******************
// ******************** EINDE ********************
   

Testen en uitvoeren

Als alles correct is uitgevoerd, kun je nu starten met alle drie onze projecten. Gebruik het consolecommando dotnet run -lp https voor elke toepassing om ze met HTTPS uit te voeren.

Nadat je alle drie de toepassingen hebt gestart, open je de BffSample toepassing in je browser (https://localhost:5003) en authenticateer je met de gegevens [email protected] en Jd!2024$3cur3. Na succesvolle authenticatie zou je de lijst moeten zien met claims die je van de authenticatieserver hebt ontvangen, zoals eerder gezien. Daaronder zie je ook de weersvoorspelling.

De weersvoorspelling wordt geleverd door de aparte toepassing ApiSample, die een toegangstoken gebruikt die is uitgegeven door de authenticatieservice OpenIDProviderApp. Het zien van de weersvoorspelling in het venster van de BffSample toepassing geeft aan dat onze SPA succesvol de backend van BffSample heeft aangeroepen, die vervolgens de aanroep naar ApiSample heeft doorgespeeld door het toegangstoken toe te voegen. ApiSample heeft de aanroep geauthenticateerd en heeft gereageerd met een JSON-bestand dat de weersvoorspelling bevat.

De volledige oplossing is beschikbaar op GitHub.

Als u tijdens het implementeren van de testprojecten problemen of fouten tegenkomt, kunt u verwezen zijn naar de complete oplossing die beschikbaar is in de GitHub-repository. Blijf de repository Abblix/Oidc.Server.GettingStarted klonen om toegang te krijgen tot de volledig geïmplementeerde projecten die in dit artikel beschreven zijn. Dit bronmiddel dient zowel als een probleemoplossingstoool als een solide startpunt voor het maken van uw eigen projecten.

Conclusie

De evolutie van authenticatieschema’s als OAuth 2.0 en OpenID Connect reflecteert de bredere trends in webveiligheid en browsercapabilities. Het afstappen van verouderde methodes zoals de Implicit Flow naar meer veilige aanpakken, zoals de Authorization Code Flow met PKCE, heeft de veiligheid significant verhoogd. Het zijn echter de inherente kwetsbaarheden van het werken in onbeheerde omgevingen die het beveiligen van moderne SPA’s een uitdaging maken. Het alleen op de backend opslaan van tokens en het overnemen van het Backend-For-Frontend (BFF) patroon is een effectieve strategie om risico’s te verminderen en de robuuste bescherming van gebruikersgegevens te waarborgen.

Ontwikkelaars moeten alert blijven bij het aanpakken van het altijd veranderende gevaarlijke landschap door nieuwe authenticatietechnieken en actuele architectuurtechnieken toe te passen. Deze voorwaardelijke aanpak is crucial voor het bouwen van veilige en betrouwbare webapplicaties. In dit artikel zijn we kennis gaan maken en een moderne aanpak geëxploreerd voor het integreren van OpenID Connect, BFF en SPA met behulp van de populaire .NET en React technologystap. Deze aanpak kan dienen als een sterke basis voor uw toekomstige projecten.

Als we naar de toekomst kijken, zal de doorlopende evolutie van webveiligheid nog meer innovatie eisen in authenticatie en architecturale patronen. We nodigen u uit om ons GitHub-repository te verkennen, bij te dragen aan de ontwikkeling van moderne authenticatiesoluties en mee te liften aan de voortdurende vooruitgang. Bedankt voor uw aandacht!

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