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:
- 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.
- 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.
- 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.
- 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:
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:
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:
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:
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
:
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
:
{
// ******************* 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
:
{
// ******************* 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: DieDefaultScheme
-Eigenschaft setzt die Authentifizierung standardmäßig mit demCookies
-Schema und weist dasDefaultChallengeScheme
an, beim Fehlschlagen der Standardauthentifizierung die Ausführung der Authentifizierung an dasOpenIdConnect
-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.OpenIdConnect
Abschnitt:SignInScheme
undSignOutScheme
Eigenschaften definieren dasCookies
Schema, das nach der Anmeldung zur Speicherung der Benutzerinformationen verwendet wird.- Die
Authority
Eigenschaft enthält die Basis-URL des OpenID Connect Servers.ClientId
undClientSecret
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 dieBffClient
Anwendung zugänglich machen möchte. In diesem Fall werden Standardbereiche wieopenid
(Benutzer-ID),profile
(Benutzerprofil) undemail
(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 vonfalse
bedeutet, dass Anspruchsklagen im authentifizierten Benutzersitzung in der Form behalten werden, in der sie vom OpenID Connect Server erhalten wurden.ResponseType
mit dem Wertcode
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:
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()
:
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:
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:
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:
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
undLogout
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 ein401 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 NamenCorsPolicyName
, 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 vorherigeCheckSession
-Aufruf401 Unauthorized
zurückgegeben hat. Er stellt sicher, dass der Benutzer immer noch nicht authentifiziert ist und startet den konfiguriertenChallenge
-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 vonBffSample
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:
// ******************* 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:
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:
{
// ******************* 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:
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
undlogout
. 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:
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:
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:
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:
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
oderbadidea
ein, je nach Ihrer Chrome-Version. Diese Tastatureingaben dienen als Umgehungskommandos in Chrome und ermöglichen Ihnen, fortzufahren auf derlocalhost
-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:
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:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Richten Sie im Datei ApiSample\Program.cs
das Authenticationschema und die Autorisierungsstrategie namens WeatherApi
ein:
// ******************* 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 einenscope
-Anspruch haben, der den Wertweather
enthält. Da derscope
-Anspruch laut RFC 8693 mehrere Werte durch Leerzeichen getrennt enthalten kann, wird der tatsächlichescope
-Wert in Einzelteile aufgeteilt und der resultierende Array wird geprüft, um den erforderlichenweather
-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:
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:
{
// ******************* 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 derApiSample
-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 auffalse
gesetzt, was bedeutet, dass claims ihren ursprünglichen Namen aus dem JWT verwenden.TokenValidationParameters
:ValidTypes
: Aufat+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 Clienthttps://localhost:5004
ausgestellt wurden.ValidIssuer
: Legt fest, dass die Anwendung Token akzeptieren wird, die vom Serverhttps://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:
// 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:
dotnet add package Yarp.ReverseProxy
Fügen Sie anschließend die folgenden Namespaces am Anfang der Datei BffSample\Program.cs
hinzu:
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:
builder.Services.AddHttpForwarder();
Und zwischen den Aufrufen von app.MapControllerRoute()
und app.MapFallbackToFile()
:
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 AbschnittOpenIdConnect:Resource
zu erhalten. Dieser Wert gibt den Adresse des Ressorts an, zu der die Anfragen weitergeleitet werden. In unserem Beispiel lautet dieser Werthttps://localhost:5004
– die Basis-URL, wo sich die AnwendungApiSample
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-HeaderAuthorization
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:
"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:
{
"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:
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. DasState
-Interface beschreibt die Struktur des Komponentenstatus, die aus einem Array von Wetterprognosen und einem Ladezeichen besteht. - Der
WeatherForecast
-Komponente wird diefetchBff
-Funktion von demuseBff
-Hook entnommen und dazu verwendet, Wetterdaten vom Server abzurufen. Der Komponentenstatus wird mit demuseState
-Hook verwaltet, indem ein leeres Array von Prognosen und ein Ladezeichen, das auf wahr gesetzt ist, initialisiert werden. - Der
useEffect
-Hook löst diefetchBff
-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 (übersetState
) und das Ladezeichen auffalse
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:
// ******************* 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