Moderne Authentifizierung auf .NET: OpenID Connect, BFF, SPA

Wie sich die Web-Technologien weiterentwickeln, so entwickeln sich auch die Methoden und Protokolle, die dafür gedacht sind, sie zu sichern. Die Protokolle OAuth 2.0 und OpenID Connect haben in Reaktion auf neu auftretende Sicherheitsbedrohungen und die zunehmende Komplexität von Webanwendungen bedeutend evolveiert. Traditionelle Authentifizierungsverfahren, die einst effektiv waren, erweisen sich mittlerweile für moderne Single Page Applications (SPAs) als veraltet, die neue Sicherheits Herausforderungen zu bewältigen haben. In diesem Zusammenhang hat sich die Architekturmuster Backend-For-Frontend (BFF) als eine empfohlene Lösung für die Organisation der Interaktionen zwischen SPAs und ihren Backend-Systemen etabliert, die eine sicherere und verwaltbare Methode für Authentifizierung und Session-Verwaltung bietet. In diesem Artikel wird das BFF-Muster im Detail untersucht und durch eine minimalistische Lösung implementiert mit .NET und React demonstriert. Am Ende werden Sie einen klaren Verständnis dafür haben, wie Sie das BFF-Muster einsetzen können, um die Sicherheit und Funktionalität Ihrer Webanwendungen zu verbessern.

Historischer Kontext

Die Geschichte von OAuth 2.0 und OpenID Connect reflektiert die anhaltende Entwicklung der Internet-Technologien. Lassen Sie uns genauer anschaun, wie diese Protokolle und ihre Auswirkungen auf moderne Webanwendungen waren.

Eingeführt im Jahr 2012 hat das OAuth 2.0-Protokoll zu einem breit angenommenen Standard für Autorisierung geworden. Es ermöglicht es Drittanwendungen, auf Nutzerressourcen mit begrenztem Zugriff zu erhalten, ohne die Nutzeranmeldeinformationen dem Client zu暴露. OAuth 2.0 unterstützt verschiedene Flows, die jeweils für verschiedene Anwendungsfälle konzipiert sind.

Auf der Basis von OAuth 2.0 entstand 2014 das Protokoll OpenID Connect (OIDC), das wesentliche Authentifizierungsfunktionen hinzufügte. Es stellt Clientanwendungen eine standardisierte Methode zur Überprüfung der Benutzeridentität und zum Erhalten grundlegender Informationen über sie durch einen standardisierten Zugriffspunkt oder durch die Erlangung eines ID-Token in JWT (JSON Web Token) Format bereit.

Entwicklung der Bedrohungsmodell

Mit den zunehmenden Fähigkeiten und Beliebtheit von SPAs hat sich das Bedrohungsmodell für SPAs ebenfalls entwickelt. Vulnerabilitäten wie Cross-Site Scripting (XSS) und Cross-Site Request Forgery (CSRF) sind immer häufiger zu finden. Da SPAs oft mit dem Server über APIs interagieren, ist es für die Sicherheit wichtig, Zugriffstoken und Aktualisierungstoken sicher zu speichern und zu verwenden.

Reagierend auf die Forderungen der Zeit sind die Protokolle OAuth und OpenID Connect weiterhin im Entwicklungsprozess, um sich an neue Herausforderungen zu adaptieren, die mit neuen Technologien und der zunehmenden Zahl von Bedrohungen entstehen. Gleichzeitig bedeutet die ständige Entwicklung von Bedrohungen und die Verbesserung von Sicherheitspraktiken, dass alte Ansätze nicht mehr den modernen Sicherheitsanforderungen entsprechen. Daher bietet das OpenID Connect Protokoll derzeit eine breite Palette von Fähigkeiten an, aber viele davon werden bereits oder werden bald veraltet und oft unsicher betrachtet. Diese Vielfalt stellt für SPA-Entwickler eine Herausforderung dar, die am besten geeignete und sichere Art zu interagieren mit dem OAuth 2.0 und OpenID Connect Server zu finden.

Insbesondere der Implizite Fluss kann nun als veraltet betrachtet werden, und für jeden Typ von Client, egal ob es sich um ein SPA, eine mobilene Anwendung oder ein Desktop-Anwendung handelt, ist es nun empfehlenswert, den Autorisierungscode-Fluss zusammen mit dem Proof Key for Code Exchange (PKCE) zu verwenden.

Sicherheit moderner SPAs

Warum werden moderne SPAs immer noch als verwundbar betrachtet, selbst wenn der Autorisierungscode-Fluss mit PKCE verwendet wird? Diese Frage hat mehrere Antworten.

JavaScript-Code-Vulnerabilitäten

JavaScript ist eine leistungsstarke Programmiersprache, die eine Schlüsselrolle in modernen Single Page Applications (SPAs) spielt. Allerdings bringt seine breite Kompetenz und Verbreitung eine potenzielle Bedrohung mit. Moderne SPAs, die auf Bibliotheken und Frameworks wie React, Vue oder Angular basieren, verwenden eine große Anzahl von Bibliotheken und Abhängigkeiten. Diese können Sie im Verzeichnis node_modules sehen, und die Anzahl dieser Abhängigkeiten kann hunderte oder sogar tausende betragen. Jede dieser Bibliotheken kann unterschiedliche Grad der Kritikalität an Vulnerabilitäten aufweisen, und SPA-Entwickler haben keine Möglichkeit, den Code aller verwendeten Abhängigkeiten gründlich zu prüfen. Oft haben Entwickler nicht selbst die volle Liste der Abhängigkeiten verfolgt, da sie untereinander transitiv abhängig sind. Selbst wenn man seinen eigenen Code bis zu den höchsten Standards an Qualität und Sicherheit entwickelt, kann man sich nicht sicher sein, dass Vulnerabilitäten in der fertigen Anwendung fehlen.

Bösartiges JavaScript-Code, der auf verschiedene Weise in eine Anwendung eingefügt werden kann, z.B. durch Angriffe wie Cross-Site Scripting (XSS) oder durch die Kompromittierung von Drittanbieterbibliotheken, erhält die gleichen Privilegien und Zugangsgrade wie legitime Anwendungscode. Dies ermöglicht dem böswilligen Code, Daten von der aktuellen Seite aus zu stehlen, mit der Anwendungs Oberfläche zu interagieren, Anfragen an das Backend zu senden, Daten aus dem lokalen Speicher (localStorage, IndexedDB) heraus zu stehlen und sogar selbst Authentisierungsverfahren zu initiieren, um eigenen Zugriffstoken mithilfe desselben Autorisierungscodes und PKCE-Flusses zu erhalten.

Spectre-Vulnerabilität

Die Spectre-Vulnerabilität nutzt Merkmale der modernen Prozessorarchitektur, um auf Daten zuzugreifen, die isoliert bleiben sollten. Solche vulnerablen Stellen sind insbesondere für SPAs gefährlich.

Erstens nutzen SPAs intensiv JavaScript, um den Anwendungszustand zu verwalten und mit dem Server zu interagieren. Dies vergrößert den Angriffsfläche für böswilligen JavaScript-Code, der Spectre-Vulnerabilitäten ausnutzen kann. Zweitens ist eine Neulade von SPA wie bei traditionellen Mehrseitenerstellungen (MPAs) selten, das heißt, die Seite und ihr geladener Code bleiben lange aktiv. Dies gibt Angreifern deutlich mehr Zeit, Angriffe mit böswilligem JavaScript-Code durchzuführen.

Spectre-Vulnerabilitäten ermöglichen es Angreifern, Zugriffstoken, die in den Speicher von JavaScript-Anwendungen gespeichert sind, auszulesen, was es ihnen ermöglicht, durch die Impersonation einer legitimen Anwendung zu geschützten Ressourcen Zugriff zu erhalten. Der Spekulative Ausführungsmechanismus kann auch verwendet werden, um Benutzer-Sitzungsdaten zu stehlen, was Angreifern ermöglicht, ihre Angriffe auch nach dem Schließen eines SPA fortzusetzen.

Die Entdeckung weiterer Vulnerabilitäten ähnlich dem Spectre in der Zukunft kann nicht ausgeschlossen werden.

Was sollte man tun?

Lassen Sie uns eine wichtige Zwischenbilanz ziehen. Moderne SPA-Anwendungen, die von vielen dritten Parteien JavaScript-Bibliotheken abhängen und in der Browserumgebung auf den Geräten der Benutzer ausgeführt werden, arbeiten in einem Software- und Hardwareumfeld, das die Entwickler nicht vollständig kontrollieren können. Daher sollten wir solche Anwendungen grundsätzlich als vulnerabel betrachten.

Als Reaktion auf die aufgeführten Bedrohungen tendieren mehr Experten dazu, es ganz vermeiden zu lassen, Token im Browser zu speichern und die Anwendung so zu entwerfen, dass Zugriffstoken und AktualisierungsToken nur vom Serverteil der Anwendung erhalten und verarbeitet werden und sie nie an die Browserseite weitergegeben werden. In der Umgebung eines SPA mit Backend kann dies mit der Backend-For-Frontend (BFF) Architekturpattern erreicht werden.

Das Interaktionsschema zwischen dem Autorisierungsserver (OP), dem Client (RP), der das BFF-Muster implementiert, und einer dritten API (Ressourcenserver) sieht so aus:

Der Einsatz des BFF-Musters zur Sicherung von SPA’s bietet mehrere Vorteile. Zugriffstoken und AktualisierungsToken werden auf dem Server gespeichert und werden nie an die Browserseite weitergegeben, was ihre Diebstahl durch vulnerablen Code verhindert. Session- und Tokenverwaltung erfolgt auf dem Server, was eine bessere Sicherheitskontrolle und eine zuverlässigere Authentifizierungsprüfung ermöglicht. Der Client-Anwendung interagiert mit dem Server über den BFF, was die Anwendungslogik vereinfacht und das Risiko des Ausführens von böswilligen Codes reduziert.

Der Backend-For-Frontend-Musteraufbau auf der .NET-Plattform umzusetzen

Bevor wir uns an der praktischen Implementierung von BFF auf der .NET-Plattform ergehen, sollten wir die notwendigen Komponenten in Betracht ziehen und unser Handeln planen. Nehmen wir an, wir verfügen bereits über einen konfigurierten OpenID Connect-Server und müssen ein SPA entwickeln, das mit einem Backend arbeitet, Implementierung der Authentifizierung mittels OpenID Connect und Organisieren der Interaktion zwischen den Server- und Clientteilen mithilfe des BFF-Muster.

Laut dem Dokument OAuth 2.0 for Browser-Based Applications setzt das BFF-Architekturmuster voraus, dass der Backend als OpenID Connect-Client agiert, den Autorisierungscodefluss mit PKCE für die Authentifizierung verwendet, Zugriff und Refresh-Token auf seiner Seite erhält und speichert und diese niemals dem SPA-Seite im Browser übermittelt. Das BFF-Muster geht auch davon aus, dass sich auf der Backend-Seite eine API befindet, die aus vier Hauptendpunkten besteht:

  1. Check Session: dient zum Prüfen einer aktiven Benutzerauthentifizierungssitzung. Typischerweise von dem SPA mit einer asynchronen API (fetch) aufgerufen und, wenn erfolgreich, Informationen über den aktiven Benutzer zurückgegeben. Damit kann das SPA, das von einem dritten Quellcode (z.B. CDN) geladen wird, die Authentifizationsstatus prüfen und entweder seine Arbeit mit dem Benutzer fortsetzen oder zur Authentifizierung mit dem OpenID Connect-Server gehen.
  2. Anmeldung: leitet den Authentisierungsprozess beim OpenID Connect-Server ein. Typischerweise, wenn der SPA die autorisierten Benutzerdaten in Schritt 1 überprüfen nicht erfolgreich erhält, leitet er den Browser auf diese URL um, die dann eine vollständige Anfrage an den OpenID Connect-Server aufbaut und dorthin umleitet.
  3. Einloggen: empfängt den Autorisierungscode, der vom Server nach Schritt 2 bei erfolgreicher Authentisierung gesendet wird.stellt eine direkte Anfrage an den OpenID Connect-Server, um den Autorisierungscode + PKCE-Code-Verifier gegen Zugriffs- und Aktualisierungs-Token zu tauschen. Erzeugt eine autorisierte Sitzung auf der Client-Seite, indem ein Authentifizierungscookie für den Benutzer ausgestellt wird.
  4. Abmelden: dient zur Beendigung der Authentisierungsverbindung. Typischerweise leitet der SPA den Browser auf diese URL um, die in ihrer turn eine Anfrage an das End-Sitzungs-Endepunkt auf dem OpenID Connect-Server sendet, um die Sitzung zu beenden, sowie die Sitzung auf der Client-Seite und den Authentifizierungscookie zu löschen.

Lassen Sie uns nun Prüfen von Tools, die die .NET-Plattform standardmäßig bereitstellt und schauen, wie wir sie verwenden können, um das BFF-Muster zu implementieren. Die .NET-Plattform bietet das NuGet-Paket Microsoft.AspNetCore.Authentication.OpenIdConnect an, das eine von Microsoft unterstützte OpenID Connect-Client-Implementierung ist. Dieses Paket unterstützt sowohl den Autorisierungscode-Fluss als auch PKCE und fügt einen Endpunkt mit der relativen Pfad /signin-oidc hinzu, der bereits die notwendige Einloggen-Endpunkt-Funktionalität implementiert (siehe oben Punkt 3). Daher müssen wir nur die drei verbleibenden Endpunkte implementieren.

Für einen praktischen Integrationsbeispiel wird ein Test-OpenID-Connect-Server auf der Basis der Abblix OIDC Server-Bibliothek genommen. Allerdings gilt jedes hier erwähnte below auch für jeden anderen Server, einschließlich öffentlich verfügbarer Server von Facebook, Google, Apple und allen anderen, die sich an die OpenID Connect-Protokollspezifikation anpassen.

Um das SPA auf der Frontendseite einzurichten, werden wir die React-Bibliothek verwenden, und auf der Backendseite werden wir .NET WebAPI verwenden. Dies ist einer der am häufigsten verwendeten Technologiestämme zum Zeitpunkt der Schreibung dieses Artikels.

Der Gesamtschematismus der Komponenten und ihrer Interaktion sieht so aus:

Um mit den Beispielen aus diesem Artikel zu arbeiten, müssen Sie auch die .NET SDK und Node.js installieren. Alle Beispiele in diesem Artikel wurden mit .NET 8, Node.js 22 und React 18 entwickelt und getestet, die zum Zeitpunkt der Schreibung aktuell waren.

Erstellen eines Client-SPA auf React mit Backend auf .NET

Um eine Clientanwendung schnellstmöglich zu erstellen, ist es bequem, ein fertiges Template zu verwenden. Bis zur Version .NET 7 bot das SDK ein integriertes Template für eine .NET WebAPI-Anwendung und einen React SPA an. Leider wurde dieses Template in der Version .NET 8 entfernt. Deshalb hat das Abblix-Team ein eigenes Template geschaffen, das einen .NET WebApi Backend-Anwendungskern, ein Frontend-SPA auf der Basis der React-Bibliothek und TypeScript mit Vite enthält. Dieses Template ist als Teil des Pakets Abblix.Templates öffentlich verfügbar und kann mit dem folgenden Befehl installiert werden:

Shell

 

dotnet new install Abblix.Templates

Jetzt können wir das Template namens abblix-react verwenden. Nutzen wir es, um eine neue Anwendung namens BffSample zu erstellen:

Shell

 

dotnet new abblix-react -n BffSample

Dieser Befehl erstellt eine Anwendung, die aus einem .NET WebApi Backend und einem React SPA-Client besteht. Die Dateien, die mit dem SPA verbunden sind, befinden sich im Verzeichnis BffSample\ClientApp.

Nach der Erstellung des Projekts wird das System Sie auffordern, einen Befehl auszuführen, um die Abhängigkeiten zu installieren:

Shell

 

cmd /c "cd ClientApp && npm install"

Diese Aktion ist notwendig, um alle erforderlichen Abhängigkeiten für den Client-Teil der Anwendung zu installieren. Für den erfolgreichen Start des Projekts ist es ratsam, diesen Befehl mit dem Eingabefenster Y (ja) zu bestätigen und auszuführen.

Lassen Sie uns die Portnummer ändern, auf der die BffSample-Anwendung lokal ausgeführt wird, auf 5003. Dieser Schritt ist nicht zwingend notwendig, erleichtert aber die weitere Konfiguration des OpenID Connect-Servers. Um dies zu tun, öffnen Sie die Datei BffSample\Properties\launchSettings.json, suchen Sie nach der Profilbezeichnung https und ändern Sie den Wert der Eigenschaft applicationUrl auf https://localhost:5003.

Nachfolgend fügt man die NuGet-Pakete hinzu, die die OpenID Connect-Client-Implementierung bereitstellen, indem man zum Verzeichnis BffSample geht und den folgenden Befehl ausführt:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect

Richte zwei Authentifizierungsschemata mit den Namen Cookies und OpenIdConnect in der Anwendung ein und lesen deren Einstellungen aus der Anwendungskonfiguration. Um dies zu tun, ändere den Datei 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();

Und füge die notwendigen Einstellungen hinzu, um mit dem OpenID Connect-Server verbunden zu werden, in der Datei 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",

Und in der Datei BffSample\appsettings.Development.json:

JSON

 

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

Lassen Sie uns kurz jeden Einstellungspunkt und ihren Zweck überprüfen:

  • Authentifizierung-Abschnitt: Die DefaultScheme-Eigenschaft setzt die Authentifizierung standardmäßig mit dem Cookies-Schema und weist das DefaultChallengeScheme an, beim Fehlschlagen der Standardauthentifizierung die Ausführung der Authentifizierung an das OpenIdConnect-Schema zu delegieren. Daher wird, wenn das Anwendungskonto unbekannt ist, beim ersten Aufruf der OpenID Connect-Server abgefragt, um die Authentifizierung durchzuführen. Anschließend erhält der authentifizierte Benutzer ein Authentifizierungs-Cookie und alle weiteren Serveraufrufe werden mit diesem Cookie autorisiert, ohne dass der OpenID Connect-Server erneut kontaktiert werden muss.
  • OpenIdConnectAbschnitt:
    • SignInScheme und SignOutScheme Eigenschaften definieren das Cookies Schema, das nach der Anmeldung zur Speicherung der Benutzerinformationen verwendet wird.
    • Die Authority Eigenschaft enthält die Basis-URL des OpenID Connect Servers. ClientId und ClientSecret geben die Identifikation und geheimen Schlüssel des Klientenanwendungen an, die auf dem OpenID Connect Server registriert sind.
    • SaveTokens zeigt an, dass die Tokens, die als Resultat der Authentifizierung vom OpenID Connect Server erhalten werden, gespeichert werden sollen.
    • Scope enthält eine Liste von Bereichen, die die BffClient Anwendung zugänglich machen möchte. In diesem Fall werden Standardbereiche wie openid (Benutzer-ID), profile (Benutzerprofil) und email (E-Mail) angefordert.
    • MapInboundClaims ist verantwortlich für die Umwandlung von eingehenden Anspruchsklagen vom OpenID Connect Server in Anspruchsklagen, die in der Anwendung verwendet werden. Ein Wert von false bedeutet, dass Anspruchsklagen im authentifizierten Benutzersitzung in der Form behalten werden, in der sie vom OpenID Connect Server erhalten wurden.
    • ResponseType mit dem Wert code zeigt an, dass der Client den Authorization Code Flow verwenden wird.
    • ResponseMode legt die Übertragung des Autorisierungscodes in der Abfragezeichenfolge fest, was den Standard für den Authorization Code Flow ist.
    • Die UsePkce Eigenschaft zeigt an, dass während der Authentifizierung PKCE verwendet werden soll, um die Abhängigkeit des Autorisierungscodes vom Browser zu vermeiden.
    • Die GetClaimsFromUserInfoEndpoint Eigenschaft zeigt an, dass die Benutzerprofildaten vom UserInfo Endpunkt bezogen werden sollen.

Da unsere Anwendung keine Interaktion mit dem Benutzer ohne Authentifizierung voraussetzt, werden wir sicherstellen, dass der React SPA nur nach erfolgreicher Authentifizierung geladen wird. Natürlich, wenn der SPA von einer externen Quelle wie einem Static Web Host geladen wird, beispielsweise von Content Delivery Network (CDN)-Servern oder einem lokalen Entwicklungsrechner, der mit dem Befehl npm start gestartet wurde (z.B. wenn unser Beispiel im Debug-Modus ausgeführt wird), ist es nicht möglich, die Authentifizierungsstatus vor dem Laden des SPA zu prüfen. Aber wenn unser eigenes .NET-Backend für den Laden des SPA zuständig ist, ist dies möglich.

Um dies zu tun, fügen Sie im Datei BffSample\Program.cs den Middleware hinzu, der für die Authentifizierung und Autorisierung zuständig ist:

C#

 

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

Am Ende der Datei BffSample\Program.cs, wo direkt der Übergang zum Laden des SPA erfolgt, fügen Sie die Autorisierungsanforderung hinzu, .RequireAuthorization():

C#

 

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

Einrichten des OpenID Connect-Servers

Wie bereits erwähnt, werden wir für das praktische Integrationsexample einen Test-OpenID-Connect-Server basierend auf der Abblix OIDC Server Bibliothek verwenden. Die Basisvorlage für eine Anwendung basierend auf ASP.NET Core MVC mit der Abblix OIDC Server Bibliothek ist ebenfalls im Abblix.Templates Paket verfügbar, das wir zuvor installiert haben. Verwenden wir diese Vorlage, um eine neue Anwendung namens OpenIDProviderApp zu erstellen:

Shell

 

dotnet new abblix-oidc-server -n OpenIDProviderApp

Um den Server zu konfigurieren, müssen wir die BffClient Anwendung als Client beim OpenID Connect Server registrieren und einen Testbenutzer hinzufügen. Um dies zu tun, fügen Sie die folgenden Blöcke dem Datei OpenIDProviderApp\Program.cs hinzu:

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

// ...

// Registrieren und Konfigurieren des Abblix OIDC Servers
builder.Services.AddOidcServices(options =>
{
    // Hier Konfigurationseinstellungen für den OIDC Server festlegen:
    // ******************* 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 ********************
    // Die folgende URL führt zu der Login-Aktion des AuthController
    options.LoginUri = new Uri($"/Auth/Login", UriKind.Relative);

    // Die folgende Zeile generiert eine neue Schlüssel für die Signatur von Tokens. Ändern Sie dies, wenn Sie eigene Schlüssel verwenden möchten.
    options.SigningKeys = new[] { JsonWebKeyFactory.CreateRsa(JsonWebKeyUseNames.Sig) };
});

Lassen Sie uns diesen Code detailliert prüfen. Wir registrieren einen Client mit der Kennung bff_sample und dem geheimen Schlüssel secret (als SHA512-Hash gespeichert), indem angegeben wird, dass der Tokenerwerb die Clientauthentifizierung mit dem geheimen Schlüssel in einem POST-Antrag verwenden wird (ClientAuthenticationMethods.ClientSecretPost). AllowedGrantTypes legt fest, dass der Client nur den Autorisierungscode-Fluss verwenden darf. ClientType definiert den Client als vertraulich, was bedeutet, dass er seinen geheimen Schlüssel sicher speichern kann. OfflineAccessAllowed erlaubt dem Client, Refreshtokens zu verwenden. PkceRequired verpflichtet die Verwendung von PKCE während der Authentifizierung. RedirectUris und PostLogoutRedirectUris enthalten Listen von zugelassenen URLs für die Weiterleitung nach der Authentifizierung bzw. beim Beenden der Sitzung.

Für jede andere OpenID Connect-Server sind die Einstellungen ähnlich, mit Unterschieden nur in der Konfiguration.

Implementierung der Basic BFF API

Früher haben wir erwähnt, dass die Verwendung des Pakets Microsoft.AspNetCore.Authentication.OpenIdConnect automatisch die Implementierung des Sign-In-Endpunkts in unsere Testanwendung hinzufügt. Jetzt ist es Zeit, den verbleibenden Teil der BFF API zu implementieren. Wir werden für diese zusätzlichen Endpunkte einen ASP.NET MVC-Controller verwenden. Beginnen wir mit dem Hinzufügen eines Ordners Controllers und einer Datei BffController.cs im Projekt BffSample, und fügen folgender Code hinein ein:

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()
    {
        // sende 401 Unauthorized zurück, um die SPA-Weiterleitung zum Anmeldungskennpunkt zu erzwingen
        if (User.Identity?.IsAuthenticated != true)
            return Unauthorized();

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

    [HttpGet("login")]
    public ActionResult> Login()
    {
        // Logik, um den Autorisierungscodefluss zu initiieren
        return Challenge(new AuthenticationProperties { RedirectUri = Url.Content("~/") });
    }

    [HttpPost("logout")]
    public IActionResult Logout()
    {
        // Logik, um den Benutzer abzumelden
        return SignOut();
    }
}

Lassen Sie uns diese Klasse in detail analysieren:

  • [Route("[controller]")]-Attribut setzt die Basisroute für alle Aktionen im Controller. In diesem Fall entspricht die Route dem Namen des Controllers, was bedeutet, dass alle Pfade zu unseren API-Methoden mit /bff/ beginnen werden.
  • Die Konstante CorsPolicyName = "Bff" definiert den Namen der CORS-(Cross-Origin Resource Sharing)-Richtlinie, die in Methodenattributen verwendet wird. Wir verweisen später darauf zurück.
  • Die drei Methoden CheckSession, Login und Logout implementieren die notwendige BFF-Funktionalität, die oben beschrieben ist. Sie behandeln GET-Anfragen an /bff/check_session, /bff/login und POST-Anfragen an /bff/logout jeweils.
  • Die Methode CheckSession überprüft den Authentifizierungsstatus des Benutzers. Wenn der Benutzer nicht authentifiziert ist, sendet sie ein 401 Unauthorized-Code zurück, der die SPA zu einer Weiterleitung zum Authentifizierungs-Endpunkt zwingen sollte. Wenn die Authentifizierung erfolgreich ist, sendet die Methode ein Satz von Anspruchsdaten und deren Werten zurück. Diese Methode beinhaltet auch eine CORS-Richtlinienbindung mit dem Namen CorsPolicyName, da der Aufruf dieser Methode跨域 sein kann und Cookie enthalten kann, die für die Benutzerauthentifizierung verwendet werden.
  • Der Login-Aufruf wird vom SPA ausgeführt, wenn der vorherige CheckSession-Aufruf 401 Unauthorized zurückgegeben hat. Er stellt sicher, dass der Benutzer immer noch nicht authentifiziert ist und startet den konfigurierten Challenge-Prozess, der in eine Redirection zum OpenID Connect-Server, eine Authentifizierung des Benutzers mit dem Authorization Code Flow und PKCE und die Ausstellung eines Authentifizierungscookies resultiert. Nach diesem Vorgang geht die Steuerung an die Wurzel unserer Anwendung "~/" zurück, die den SPA aktualisiert und mit einem authentifizierten Benutzer startet.
  • Der Logout-Aufruf wird ebenfalls vom SPA ausgelöst und beendet die aktuelle Authentifizierungssitzung. Er entfernt die vom Serverteil von BffSample ausgestellten Authentifizierungscookies und ruft auch den EndSession-Endpunkt auf der OpenID Connect-Serverseite auf.

CORS-Konfiguration für BFF

Wie oben erwähnt, ist der CheckSession-Aufruf für asynchrone Aufrufe aus dem SPA gedacht (normalerweise mit dem Fetch API). Die korrekte Funktionsweise dieses Methoden hängt ab von der Fähigkeit, Authentifizierungscookies vom Browser senden zu können. Wenn der SPA von einer separaten Statischen Web-Hosting-Dienstleistung geladen wird, wie z.B. einer CDN oder einem Dev Server, der auf einer separaten Port läuft, wird dieser Aufruf zu einem Cross-Domain-Aufruf. Dies macht die Konfiguration einer CORS-Strategie notwendig, ohne die das SPA diese Methode nicht aufrufen kann.

Wir haben bereits in der Controller-Datei Controllers\BffController.cs angegeben, dass die CORS-Strategie mit dem Namen CorsPolicyName = "Bff" verwendet werden soll. Jetzt ist es Zeit, die Parameter dieser Strategie zu konfigurieren, um unser Ziel zu lösen. Lassen Sie uns zu der Datei BffSample/Program.cs zurückkehren und die folgenden Codeblöcke hinzufügen:

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

Dieser Code erlaubt es, die CORS-Politik-Methoden von SPAs, die von Quellen geladen werden, die in der Konfiguration als Array von Strings angegeben sind CorsSettings:AllowedOrigins, mit dem GET-Methode aufzurufen und erlaubt es, dass Cookies in diesem Aufruf versandt werden. Zusätzlich stellen Sie sicher, dass der Aufruf von app.UseCors(...) direkt vor app.UseAuthentication() platziert wird:

C#

 

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

Um sicherzustellen, dass die CORS-Politik korrekt funktioniert, fügen Sie die entsprechende Einstellung zum BffSample\appsettings.Development.json-Konfigurationsdatei hinzu:

JSON

 

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

In unserem Beispiel lautet die Adresse https://localhost:3000, an der der mit dem Befehl npm run dev gestartete dev Server mit dem React SPA läuft. Diese Adresse finden Sie in Ihrem Fall, indem Sie die BffSample.csproj-Datei öffnen und den Wert des SpaProxyServerUrl-Parameters finden. In einer realen Anwendung kann die CORS-Politik die Adresse Ihres CDN (Content Delivery Networks) oder eines ähnlichen Dienstes beinhalten. Wichtig ist, dass Sie sich merken, wenn Ihr SPA von einer anderen Adresse und einem anderen Port geladen wird als der, die das BFF-API bereitstellt, dass Sie diese Adresse zur CORS-Politik-Konfiguration hinzufügen müssen.

Implementierung der Authentifizierung via BFF in einer React-Anwendung

Wir haben die BFF-API auf dem Server implementiert. Nun sollten wir uns auf das React-SPA konzentrieren und die entsprechende Funktionalität hinzufügen, um auf diese API zuzugreifen. Beginnen wir mit dem Navigieren in den Ordner BffSample\ClientApp\src\, wo wir ein components-Verzeichnis erzeugen und eine Datei Bff.tsx mit dem folgenden Inhalt hinzufügen:

TypeScript

 

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

// Definition der Form des BFF-Kontexts
interface BffContextProps {
    user: any;
    fetchBff: (endpoint: string, options?: RequestInit) => Promise;
    checkSession: () => Promise;
    login: () => void;
    logout: () => Promise;
}

// Erstellung eines Kontexts für BFF, um Status und Funktionen über die gesamte Anwendung zu teilen
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);

    // Basis-URL normalisieren, indem ein abschließender Schrägstrich entfernt wird, um inkonsistente URLs zu vermeiden
    if (baseUrl.endsWith('/')) {
        baseUrl = baseUrl.slice(0, -1);
    }

    const fetchBff = async (endpoint: string, options: RequestInit = {}): Promise => {
        try {
            // Die fetch-Funktion enthält Zertifikate, um mit Cookies zu arbeiten, die für die Authentifizierung notwendig sind
            return await fetch(`${baseUrl}/${endpoint}`, {
                credentials: 'include',
                ...options
            });
        } catch (error) {
            console.error(`Error during ${endpoint} call:`, error);
            throw error;
        }
    };

    // Die login-Funktion leitet den Benutzer zur Anmeldungsseite weiter, wenn er sich autentifizieren muss
    const login = (): void => {
        window.location.replace(`${baseUrl}/login`);
    };

    // Die checkSession-Funktion ist verantwortlich für die Überprüfung der Benutzer-Sitzung beim ersten Rendern
    const checkSession = async (): Promise => {
        const response = await fetchBff('check_session');

        if (response.ok) {
            // Wenn die Sitzung gültig ist, aktualisiere den Benutzer-Status mit den empfangenen Zertifikatdaten
            setUser(await response.json());
        } else if (response.status === 401) {
            // Wenn der Benutzer nicht autentifiziert ist, leite ihn zur Anmeldungsseite weiter
            login();
        } else {
            console.error('Unexpected response from checking session:', response);
        }
    };

    // Funktion zum Abmelden des Benutzers
    const logout = async (): Promise => {
        const response = await fetchBff('logout', { method: 'POST' });

        if (response.ok) {
            // Leite nach erfolgreichem Abmelden auf die Startseite weiter
            window.location.replace('/');
        } else {
            console.error('Logout failed:', response);
        }
    };

    // useEffect wird verwendet, um die checkSession-Funktion einmalig zu führen, wenn der Komponentenmounted
    // Dies stellt sicher, dass die Sitzung sofort geprüft wird, wenn die App geladen wird
    useEffect(() => { checkSession(); }, []);

    return (
        // Bereitstellung des BFF-Kontexts mit relevanten Werten und Funktionen, die in der gesamten Anwendung verwendet werden
        
            {children}
        
    );
};

// benutzerdefinierte Schraubenzieher, um den BFF-Kontext einfach in anderen Komponenten zu verwenden
export const useBff = (): BffContextProps => useContext(BffContext);

// Exportiere HOC, um Zugriff auf den BFF-Kontext zu bieten
export const withBff = (Component: React.ComponentType) => (props: any) =>
    
        {context => }
    ;

Diese Datei exportiert:

  • Die BffProvider-Komponente, die einen Kontext für BFF erstellt und Funktionen und Statusbereich für die Authentifizierung und Sitzungsverwaltung für die gesamte Anwendung bereitstellt.
  • Der benutzerdefinierte Hook useBff(), der ein Objekt mit dem aktuellen Benutzerzustand und Funktionen zur Arbeit mit BFF zurückgibt: checkSession, login und logout. Er ist für die Verwendung in funktionalen React-Komponenten gedacht.
  • Der Higher-Order Component (HOC) withBff für die Verwendung in klassenbasierten React-Komponenten.

Erstens erstellen Sie eine UserClaims-Komponente, die nach erfolgreicher Authentisierung die Anmeldeinformationen des aktuellen Benutzers anzeigt. Erstellen Sie die Datei UserClaims.tsx im Verzeichnis BffSample\ClientApp\src\components mit dem folgenden Inhalt:

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

Dieser Code überprüft, ob ein angemeldeter Benutzer vorhanden ist, indem er den Hook useBff() verwendet, und zeigt die Anmeldeinformationen des Benutzers als Liste an, wenn der Benutzer angemeldet ist. Wenn die Benutzerdaten noch nicht verfügbar sind, zeigt er den Text Checking user session... an.

Nun gehen wir zur Datei BffSample\ClientApp\src\App.tsx über. Ersetzen Sie den Inhalt mit dem notwendigen Code. Importieren Sie BffProvider aus components/Bff.tsx und UserClaims aus components/UserClaims.tsx, und fügen Sie den Hauptkomponentencode hinzu:

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 legt der Parameter baseUrl die Basis-URL unserer BFF-API https://localhost:5003/bff fest. Diese Vereinfachung ist intentional und zielt auf die Vereinfachung ab. In einer echten Anwendung sollten Sie diese Einstellung dynamisch bereitstellen, anstatt sie hard-coded festzulegen. Es gibt verschiedene Möglichkeiten, dies zu erreichen, aber eine Diskussion darüber ist hierbei nicht in den Rahmen einer Artikelentwicklung.

Der Abmelden-Button ermöglicht dem Benutzer das Abmelden. Er ruft die durch den useBff-Hook verfügbare logout-Funktion auf und leitet den Browser des Benutzers auf die /bff/logout-Endpunkt weiter, der das Session auf dem Server beendet.

An dieser Stelle kann nun die BffSample-Anwendung zusammen mit der OpenIDProviderApp ausgeführt werden und ihre Funktionalität getestet werden. Man kann den Befehl dotnet run -lp https in jedem Projekt oder in der bevorzugten IDE verwenden, um sie zu starten. Beide Anwendungen müssen gleichzeitig laufen.

Danach öffnen Sie Ihren Browser und navigieren Sie zu https://localhost:5003. Wenn alles korrekt eingestellt ist, wird die SPA geladen und auf /bff/check_session zugreifen. Der /check_session-Endpunkt wird eine 401-Antwort zurückgeben, was die SPA dazu bringt, den Browser auf /bff/login zu weiterleiten, was dann die Authentisierung auf dem Server über den OpenID Connect Authorization Code Flow mit PKCE einleitet. Diese Abfragereihenfolge können Sie durch das Öffnen der Entwicklungskonsole Ihres Browsers und das Klicken auf den Netzwerkabschnitt beobachten. Nach erfolgreichem Eingeben der Benutzerdaten ([email protected], Jd!2024$3cur3) kehrt die Steuerung an die SPA zurück, und Sie sehen die Assertion des authentifizierten Benutzers im Browser an:

Plain Text

 

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

Zusätzlich wird der Browser durch Klicken auf den Abmelden-Button auf /bff/logout weitergeleitet, was den Benutzer abmeldet und Sie erneut auf die Login-Seite mit einem Antrag einloggen zu gehen sehen werden.

Wenn Sie bei der Arbeit mit Ihrem Code auf Probleme stoßen, können Sie Ihren Code mit dem in unserem GitHub-Repository Abblix/Oidc.Server.GettingStarted enthaltenen Code vergleichen, der dies und andere Beispiele bereitgestellt hat, die ausführbar sind.

Beheben von HTTPS-Zertifikatvertrauensproblemen

Wenn Sie lokal Webanwendungen testen, die mit HTTPS ausgeführt werden, können Sie in Ihrem Browser Warnungen erhalten, dass das SSL-Zertifikat nicht vertrauenswürdig ist. Dieser Fehler tritt auf, weil die von ASP.NET Core verwendeten Entwicklungszertifikate nicht von einer anerkannten Zertifizierungsstelle (CA) ausgestellt sind, sondern selbstsigniert oder gar nicht im System vorhanden sind. Diese Warnungen können durch die Ausführung des folgenden Befehls gelöst werden:

Shell

 

dotnet dev-certs https --trust

Mit diesem Befehl wird ein selbstsigniertes Zertifikat für localhost generiert und installiert, sodass das System auf dieses Zertifikat vertraut. Dieses Zertifikat wird von ASP.NET Core verwendet, um lokale Webanwendungen auszuführen. Nach der Ausführung dieses Befehls müssen Sie Ihren Browser neu starten, damit die Änderungen wirksam werden.

Hinweis für Chrome-Nutzer: Auch nach dem Installieren des Entwicklungszertifikats als vertraut, können einige Chrome-Versionen aus Sicherheitsgründen den Zugriff auf localhost-Seiten einschränken. Wenn Sie auf eine Fehlermeldung stoßen, die anzeigt, dass Ihre Verbindung nicht sicher ist und der Zugriff auf localhost durch Chrome blockiert ist, können Sie dies wie folgt umgehen:

  • Klicken Sie irgendwo auf die Fehlerseite und geben Sie thisisunsafe oder badidea ein, je nach Ihrer Chrome-Version. Diese Tastatureingaben dienen als Umgehungskommandos in Chrome und ermöglichen Ihnen, fortzufahren auf der localhost-Seite.

Es ist wichtig, diese Umgehungsmethoden nur in Entwicklungsumgebungen zu verwenden, da sie echte Sicherheitsrisiken darstellen können.

Aufruf von Drittanbieter-APIs via BFF

Wir haben erfolgreich die Authentisierung in unserer BffSample-Anwendung implementiert. Nun gehen wir dazu über, eine Drittanbieter-API aufzurufen, die Zugriffstoken benötigt.

Stellen Sie sich vor, wir haben einen separaten Dienst, der die notwendigen Daten bereitstellt, wie z.B. Wettervorhersagen, und Zugriff darauf wird nur mit einem Zugriffstoken gewährt. Die Rolle des Serverteils von BffSample wird darin bestehen, als Reverse Proxy zu agieren, d.h., er nimmt und autentifiziert den Datenanforderungstransport der SPA auf, fügt dem Zugriffstoken hinzu, leitet diesen Anforderungstransport an den Wetterdienst weiter und gibt dann die Antwort des Dienstes an die SPA zurück.

Erstellen des ApiSample-Diensts

Bevor wir den Remote-API-Aufruf über den BFF demonstrieren, müssen wir eine Anwendung erstellen, die als dieser API in unserem Beispiel dienen wird.

Um die Anwendung zu erstellen, werden wir ein Template von .NET verwenden. Navigieren Sie zu dem Verzeichnis, das die Projekte OpenIDProviderApp und BffSample enthält, und führen Sie den folgenden Befehl aus, um die ApiSample-Anwendung zu erstellen:

Shell

 

dotnet new webapi -n ApiSample

Diese ASP.NET Core Minimal API-Anwendung bietet ein einzelnes Endpunkt mit der Pfad-URL /weatherforecast an, der Wetterdaten im JSON-Format bereitstellt.

Zunächst sollten Sie den zufällig zugewiesenen Portnummer, die die ApiSample-Anwendung lokal verwendet, auf eine feste Portnummer 5004 ändern. Wie bereits erwähnt ist dieser Schritt nicht obligatorisch, erleichtert jedoch unser Setup. Um dies zu tun, öffnen Sie die Datei ApiSample\Properties\launchSettings.json, suchen Sie das Profil mit dem Namen https und ändern Sie den Wert der Eigenschaft applicationUrl auf https://localhost:5004.

Nun werden wir die Wetter-API so einschränken, dass nur mit einem Zugriffstoken zugegriffen werden kann. Navigieren Sie zum Projektordner der ApiSample und fügen Sie das NuGet-Paket für die JWT-Besitzer-Token-Authentifizierung hinzu:

Shell

 

dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer

Richten Sie im Datei ApiSample\Program.cs das Authenticationschema und die Autorisierungsstrategie namens WeatherApi ein:

C#

 

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

// Services zum Container hinzufügen.
// Erfahren Sie mehr über die Konfiguration von Swagger/OpenAPI unter 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();

Dieser Codeblock setzt die Authentifizierung indem er Konfigurationen aus den Anwendungseinstellungen lesen, Autorisierung mit JWT (JSON Web Tokens) einschließt und eine Autorisierungsstrategie namens WeatherApi einrichtet. Die Autorisierungsstrategie WeatherApi setzt die folgenden Anforderungen:

  • policy.RequireAuthenticatedUser(): Stellt sicher, dass nur angemeldete Benutzer auf geschützte Ressourcen zugreifen können.
  • policy.RequireAssertion(context => ...): Der Benutzer muss einen scope-Anspruch haben, der den Wert weather enthält. Da der scope-Anspruch laut RFC 8693 mehrere Werte durch Leerzeichen getrennt enthalten kann, wird der tatsächliche scope-Wert in Einzelteile aufgeteilt und der resultierende Array wird geprüft, um den erforderlichen weather-Wert enthalten zu haben.

Diese Bedingungen zusammen sichern, dass nur authentifizierte Benutzer mit einem Zugriffstoken, der für den weather-Bereich autorisiert ist, den durch diese Politik geschützten Endpunkt aufrufen können.

Wir müssen diese Politik auf den Endpunkt /weatherforecast anwenden. Fügen Sie den Aufruf zu RequireAuthorization() wie unten gezeigt hinzu:

C#

 

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

// ...

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

Fügen Sie die notwendigen Konfigurationseinstellungen für das Authenticationschema in die appsettings.Development.json-Datei der ApiSample-Anwendung hinzu:

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",

Lassen Sie uns jeden Einstellungsdatenpunkt auf Details untersuchen:

  • Authority: Dies ist die URL, die auf den OpenID Connect-Autorisierungsserver verweist, der JWT-Token ausgibt. Der in der ApiSample-Anwendung konfigurierte Authentifizierungspool wird diese URL verwenden, um die Informationen zu erhalten, die erforderlich sind, um Token zu verifizieren, wie z.B. Signaturschlüssel.
  • MapInboundClaims: Diese Einstellung steuert, wie auf incoming claims von dem JWT-Token abgehandelt werden, die in ASP.NET Core zu internalen claims gemappt werden. Es ist auf false gesetzt, was bedeutet, dass claims ihren ursprünglichen Namen aus dem JWT verwenden.
  • TokenValidationParameters:
    • ValidTypes: Auf at+jwt festgelegt, was gemäß RFC 9068 2.1 bedeutet, dass es sich um einen Zugriffstoken im JWT-Format handelt.
    • ValidAudience: Legt fest, dass die Anwendung Token akzeptieren wird, die für den Client https://localhost:5004 ausgestellt wurden.
    • ValidIssuer: Legt fest, dass die Anwendung Token akzeptieren wird, die vom Server https://localhost:5001 ausgestellt wurden.

Weitere Konfiguration von OpenIDProviderApp

Die Kombination aus dem Authentifizierungsservice OpenIDProviderApp und der Clientanwendung BffSample funktioniert gut zur Bereitstellung von Benutzerauthentifizierung. Allerdings ist es notwendig, um Anfragen an eine remote API zu ermöglichen, die ApiSample-Anwendung als Ressource beim OpenIDProviderApp zu registrieren. In unserem Beispiel verwenden wir den Abblix OIDC Server, der RFC 8707: Resource Indicators for OAuth 2.0 unterstützt. Daher werden wir die ApiSample-Anwendung mit dem Bereich weather als Ressource registrieren. Wenn Sie einen anderen OpenID Connect Server verwenden, der Resource Indicators nicht unterstützt, ist es immer empfehlenswert, für diese remote API einen eindeutigen Bereich zu registrieren (wie z.B. weather in unserem Beispiel).

Fügen Sie den folgenden Code zum Datei OpenIDProviderApp\Program.cs hinzu:

C#

 

// Registrieren und Konfigurieren des Abblix OIDC Servers
builder.Services.AddOidcServices(options => {
    // ******************* START *******************
    options.Resources =
    [
        new(new Uri("https://localhost:5004", UriKind.Absolute), new ScopeDefinition("weather")),
    ];
    // ******************** END ********************
    options.Clients = new[] {
        new ClientInfo("bff_sample") {

In diesem Beispiel registrieren wir die Anwendung ApiSample, indem wir ihren Basis-Adresse https://localhost:5004 als Ressource angeben und einen spezifischen Bereich namens weather definieren. In reellen Anwendungen, insbesondere solchen mit komplizierten APIs, die aus vielen Endpunkten bestehen, ist es ratsam, für jeden einzelnen Endpunkt oder jedes relatede Gruppen von Endpunkten separate Bereiche zu definieren. Diese Vorgehensweise ermöglicht eine präzisere Zugriffskontrolle und bietet Flexibilität in der Verwaltung der Zugriffsrechte. Zum Beispiel können Sie unterschiedliche Bereiche für verschiedene Operationen, Anwendungskomponenten oder Benutzerzugangsstufen erzeugen, was eine feinere Steuerung der Zugriffsberechtigungen ermöglicht.

Erläuterung von BffSample für die Proxying Anfragen an eine Remote-API

Die Clientanwendung BffSample muss nun mehr als nur einen Zugriffstoken für ApiSample anfordern. Sie muss auch Anfragen des SPA an die Remote-API verwalten. Dies erfordert das Hinzufügen des von dem Dienst OpenIDProviderApp erhaltenen Zugriffstokens zu diesen Anfragen, deren Weiterleitung an den fremden Server und die Rückgabe der Antworten des Servers an den SPA. Im Kern muss BffSample also als Reverse-Proxy-Server fungieren.

Anstatt die Anfrage-Proxydaten in unserer Clientanwendung manuell zu implementieren, werden wir YARP (Yet Another Reverse Proxy) verwenden, ein fertiges Produkt, das von Microsoft entwickelt wurde. YARP ist ein Reverse-Proxy-Server, der in .NET geschrieben ist und als NuGet-Paket verfügbar ist.

Um YARP in der Anwendung BffSample zu verwenden, fügen Sie zunächst das NuGet-Paket hinzu:

Shell

 

dotnet add package Yarp.ReverseProxy

Fügen Sie anschließend die folgenden Namespaces am Anfang der Datei BffSample\Program.cs hinzu:

C#

 

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

Bevor der Aufruf var app = builder.Build(); erfolgt, fügen Sie den Code hinzu:

C#

 

builder.Services.AddHttpForwarder();

Und zwischen den Aufrufen von app.MapControllerRoute() und app.MapFallbackToFile():

C#

 

app.MapForwarder(
    "/bff/{**catch-all}",
    configuration.GetValue("OpenIdConnect:Resource") ?? throw new InvalidOperationException("Unable to get OpenIdConnect:Resource from current configuration"),
    builderContext =>
    {
        // Entfernen Sie das Präfix "/bff" aus der Anfragepfad
        builderContext.AddPathRemovePrefix("/bff");

        builderContext.AddRequestTransform(async transformContext =>
        {
            // Holen Sie den Zugriffstoken, der zuvor während des Authentifizierungsprozesses erhalten wurde
            var accessToken = await transformContext.HttpContext.GetTokenAsync(OpenIdConnectParameterNames.AccessToken);
            
            // Fügen Sie ein Kopfzeile mit dem Zugriffstoken zum Proxyanfrage hinzu
            transformContext.ProxyRequest.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
        });
    }).RequireAuthorization();

Lassen Sie uns den Code aufbrechen, um zu sehen, was er tut:

  • builder.Services.AddHttpForwarder() registriert die notwendigen YARP-Dienste im DI-Container.
  • app.MapForwarder richtet die Anfrageumleitung zu einem anderen Server oder Endpunkt ein.
  • "/bff/{**catch-all}" ist das Pfadmuster, auf das der Reverse Proxy reagiert. Alle Anfragen, die mit /bff/ beginnen, werden von YARP verarbeitet. {**catch-all} wird verwendet, um alle verbleibenden Teile der URL nach /bff/ zu erfassen.
  • configuration.GetValue<string>("OpenIdConnect:Resource") verwendet die Anwendungskonfiguration, um den Wert aus dem Abschnitt OpenIdConnect:Resource zu erhalten. Dieser Wert gibt den Adresse des Ressorts an, zu der die Anfragen weitergeleitet werden. In unserem Beispiel lautet dieser Wert https://localhost:5004 – die Basis-URL, wo sich die Anwendung ApiSample befindet.
  • builderContext => ... fügt die notwendigen Transformationen hinzu, die YARP bei jeder eingehenden Anfrage vom SPA durchführt. In unserem Fall wird es zwei solche Transformationen geben:
    • builderContext.AddPathRemovePrefix("/bff") entfernt das Präfix /bff vom ursprünglichen Anfragepfad.
    • builderContext.AddRequestTransform(async transformContext => ...) fügt der Anfrage einen HTTP-Header Authorization hinzu, der das Zugriffstoken enthält, das zuvor während der Authentifizierung erhalten wurde. Auf diese Weise werden Anfragen vom SPA an die entfernte API mit dem Zugriffstoken authentifiziert, obwohl das SPA selbst keinen Zugriff auf dieses Token hat.
  • .RequireAuthorization() gibt an, dass für alle weitergeleiteten Anfragen eine Autorisierung erforderlich ist. Nur autorisierte Benutzer können auf die Route /bff/{**catch-all} zugreifen.

Um beim Authentifizierungsprozess ein Zugriffstoken für das Resource https://localhost:5004 anzufordern, sollten Sie den Parameter Resource mit dem Wert https://localhost:5004 zum OpenIdConnect-Konfigurationseintrag in der Datei BffSample/appsettings.Development.json hinzufügen:

JSON

 

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

Außerdem sollten Sie einen weiteren Wert weather zum Array scope in der Datei BffSample/appsettings.json hinzufügen:

JSON

 

{
  "OpenIdConnect": {

    // ...

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

    // ...

  }
}

Hinweise: In einem echten Projekt ist es notwendig, die Verfallszeit des Zugriffstokens zu überwachen. Wenn der Token demnächst verfallen ist, sollten Sie entweder im Vorfeld einen neuen mit einem Refreshtoken vom Authentifizierungsservice anfordern oder eine Zugriffsverweigerungsfehlerbehandlung vom fremden API verarbeiten, indem Sie ein neues Token erhalten und den ursprünglichen Anfrageversuch wiederholen. Aus Zweckmäßigkeit wurde dieser Aspekt in diesem Artikel bewusst ausgelassen.

Anfrage der Wetter-API via BFF in der SPA-Anwendung

Der Backend ist nun fertiggestellt. Wir haben die ApiSample-Anwendung, die eine API mit tokenbasierter Autorisierung implementiert, und die BffSample-Anwendung, die einen eingebetteten Reverse Proxy Server enthält, um sichere Zugriffe auf diese API zu ermöglichen. Der letzte Schritt besteht darin, die Funktionalität hinzuzufügen, um auf diese API zuzugreifen und die erhaltenen Daten innerhalb der React SPA anzuzeigen.

Füge die Datei WeatherForecast.tsx in BffSample\ClientApp\src\components mit dem folgenden Inhalt hinzu:

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

Lass uns diesen Code aufbrechen:

  • Das Forecast-Interface definiert die Struktur der Wetterprognosedaten, die den Tag, die Temperatur in Celsius und Fahrenheit sowie eine Zusammenfassung des Wetters beinhalten. Das State-Interface beschreibt die Struktur des Komponentenstatus, die aus einem Array von Wetterprognosen und einem Ladezeichen besteht.
  • Der WeatherForecast-Komponente wird die fetchBff-Funktion von dem useBff-Hook entnommen und dazu verwendet, Wetterdaten vom Server abzurufen. Der Komponentenstatus wird mit dem useState-Hook verwaltet, indem ein leeres Array von Prognosen und ein Ladezeichen, das auf wahr gesetzt ist, initialisiert werden.
  • Der useEffect-Hook löst die fetchBff-Funktion aus, wenn die Komponente installiert wird, und ruft Wetterprognosedaten vom Server vom /bff/weatherforecast-Endpunkt ab. Sobald die Antwort des Servers erhalten und in JSON umgewandelt wurde, werden die Daten im Komponentenstatus gespeichert (über setState) und das Ladezeichen auf false aktualisiert.
  • Abhängig vom Wert des Ladezeichens wird die Komponente entweder eine „Lade…“-Nachricht anzeigen oder eine Tabelle mit den Wetterprognosedaten rendern. Die Tabelle enthält Spalten für den Tag, die Temperatur in Celsius und Fahrenheit und eine Zusammenfassung des Wetters für jede Prognose.

Nun füge die WeatherForecast-Komponente zu BffSample\ClientApp\src\App.tsx hinzu:

TypeScript

 

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

// ...

    
// ******************* START *******************
// ******************** END ********************
   

Ausführung und Testen

Wenn alles richtig gemacht wurde, können Sie nun alle drei unsere Projekte starten. Verwenden Sie den Konsole Befehl dotnet run -lp https für jede Anwendung, um sie mit HTTPS auszuführen.

Nach dem Start aller drei Anwendungen öffnen Sie die Anwendung BffSample im Browser (https://localhost:5003) und melden Sie sich mit den Anmeldeinformationen [email protected] und Jd!2024$3cur3 an. Nach erfolgreicher Authentifizierung sollten Sie die Liste der Anträge sehen, die Sie vom Authentifizierungsserver erhalten haben, wie zuvor beschrieben. Darunter sollten Sie auch den Wettervorhersage sehen.

Der Wettervorhersage wird von der separaten Anwendung ApiSample bereitgestellt, die ein Zugriffstoken verwendet, das von der Authentifizierungsservice OpenIDProviderApp ausgestellt wird. Die Ansicht der Wettervorhersage im Fenster der BffSample Anwendung zeigt, dass unser SPA erfolgreich die Backend-Anwendung BffSample aufgerufen und dann den Aufruf an ApiSample weitergeleitet hat, indem ein Zugriffstoken hinzugefügt wurde. ApiSample hat den Aufruf authentifiziert und hat mit einem JSON mit der Wettervorhersage geantwortet.

Die komplette Lösung ist auf GitHub verfügbar.

Wenn Sie während der Implementierung der Testprojekte auf Probleme oder Fehler stoßen, können Sie sich an die vollständige Lösung in der GitHub-Repository erinnern. Klonen Sie einfach das Repository Abblix/Oidc.Server.GettingStarted, um Zugriff auf die vollständig implementierten Projekte zu erhalten, die in diesem Artikel beschrieben werden. Dieses Resource dient sowohl als Troubleshooting-Tool als auch als solide Startpunkt für die Erstellung Icher eigenen Projekte.

Fazit

Die Entwicklung von Authentifizierungsprotokollen wie OAuth 2.0 und OpenID Connect widerspiegelt den allgemeinen Trends in Web-Sicherheit und Browserfunktionen. Der Weg vonveralteten Methoden wie dem Implicit Flow hin zu sichereren Ansätzen wie dem Autorisierungscodefluss mit PKCE hat die Sicherheit erheblich verbessert. Allerdings werden die grundlegenden Vulnerabilitäten eines in unkontrollierten Umgebungen arbeitenden Systems das Sichern moderner SPA eine Herausforderung machen. Der Exklusivspeicherung von Tokens ausschließlich auf dem Backend und der Annahme des Backend-For-Frontend (BFF)-Musters ist eine effektive Strategie, um Risiken zu mindern und eine robuste Datenschutzsicherung zu gewährleisten.

Entwickler müssen weiterhin wachsam sein, indem sie die sich ständig verändernde Bedrohungslandschaft behandeln, indem sie neue Authentifizierungsmethoden und aktuelle architektonische Ansätze implementieren. Dieser proaktive Ansatz ist entscheidend, um sichere und zuverlässige Webanwendungen aufzubauen. In diesem Artikel haben wir eine moderne Methode zur Integration von OpenID Connect, BFF und SPA mit der populären .NET und React-Technologiestack untersucht und implementiert. Dieser Ansatz kann als eine solide Grundlage für Ihre künftigen Projekte dienen.

Während wir in die Zukunft blicken, wird die weitere Entwicklung der Web-Sicherheit eine immer größere Innovation in der Authentifizierung und den architektonischen Mustern fordern. Wir ermutigen Sie, unser GitHub-Repository zu durchsuchen, an der Entwicklung moderner Authentifizierungslösungen teilzunehmen und mit den anhaltenden Fortschritten vertraut zu bleiben. Vielen Dank für Ihre Aufmerksamkeit!

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