Blog

Authentifizierung für Microfrontends und Frontend-Modulithen

15 Sep 2022

Authentifizierungs-Gateways übernehmen transparent die Authentifizierung von Benutzern. Das gestaltet die Implementierung einfacher, aber auch sicherer. Dank Tokenaustausch haben sie für jede Domäne das richtige Access-Token parat.

Microfrontend-Architekturen untergliedern eine große Lösung in mehrere Frontends. Das hilft beim Reduzieren der Komplexität und steigert die Flexibilität. Allerdings möchte sich der Benutzer zur Laufzeit nicht bei jedem einzelnen Microfrontend erneut anmelden müssen. Tokenbbasierte Authentifizierung kann hier helfen. Aber wie genau lässt sich dieses Konzept für Microfrontend-Architekturen implementieren?

In diesem Artikel zeige ich zunächst ein paar Varianten auf und diskutiere deren Konsequenzen. Basierend darauf wählen wir eine Variante, die sich gerade bei Microfrontend-Architekturen, aber auch bei anderen großen Lösungen, z. B. Modulithen (modulare Monolithen), besonders gut zu eignen scheint. Dabei veranschauliche ich den Einsatz eines Authentifizierungs-Gateways – eine generische Lösung, die alle schwierigen Aspekte der Authentifizierung kapselt und sowohl die (Micro-)Frontends als auch die adressierten APIs entlastet. Außerdem veranschaulicht das Beispiel die Nutzung des Tokenaustauschs, sodass wir für jede Domäne ein eigenes Access-Token nutzen können.

Als Identity Provider kommt Keycloak zum Einsatz. Dieselben Konzepte lassen sich aber auch mit anderen Lösungen, die die Standards OAuth 2 und OpenID Connect unterstützen, umsetzen. Der Quellcode des Gateways findet sich unter [1] und der Quellcode der Microfrontends unter [2] (Kasten: „Andere Identity Provider“).

Andere Identity Provider

Unter [1] findet sich auch eine Gateway-Konfiguration, die nicht nur erfolgreich mit Keycloak, sondern auch mit anderen Identity-Lösungen getestet wurde. Eine davon ist das Cloud-basierte Azure Active Directory. Hier basiert der Tokenaustausch nicht wie bei Keycloak auf [2], sondern lose auf [3]. Unterm Strich sind beide Implementierungen zwar sehr ähnlich, zumal der Tokenaustausch über eine POST-Anfrage erfolgt. Im Detail unterscheiden sich die Parameter jedoch. Deswegen wurde der hierfür verwendete Service austauschbar gestaltet.

Auth0 bietet derzeit keine Möglichkeit zum Tokenaustausch an. Hier müsste man sich anders behelfen. Eine prinzipielle Umsetzung des Gateways mit einer Beispielkonfiguration für Auth0 findet sich jedoch unter auch [1].

Der mittlerweile kommerzielle Identity Server [4] kommt mit einer eigenen Gateway-Lösung, die die hier besprochenen Use-Cases abdeckt. Hierbei ist von einem Backend for Frontend (BFF) [5] die Rede. Ein Tokenaustausch lässt sich hier als Erweiterung [6] mit ein paar Zeilen Code einbringen.

Newsletter

Jetzt anmelden & regelmäßig wertvolle Insights in Angular sowie aktuelle Weiterbildungen erhalten!

Optionen

Große Geschäftsanwendungen verwalten in den wenigsten Fällen Ihre Benutzerkonten selbst, sondern interagieren mit existierenden Identity Providern wie Keycloak oder Active Directory. Üblicherweise stellt der Identity Provider Securitytokens aus, die den Client über die Identität des Benutzers informieren sowie Zugriff auf das Backend geben.

Für die Nutzung von Tokens in Microfrontend-Architekturen ergeben sich mehrere Möglichkeiten (Abb. 1). Zum einen können die Tokens direkt im Browser oder in einer Sitzung im Backend verwaltet werden. Die direkte Verwaltung im Browser ist sehr geradlinig. Allerdings ist sie auch anfällig für Angriffe, zumal der Browser keine sichere Möglichkeit zum Verstauen von sensiblen Informationen wie Tokens bietet. Ein Angreifer könnte die Tokens somit via XSS entwenden und daraufhin im Namen des Benutzers auftreten.

steyer_authentifizierung_1.tif_fmt1.jpg
Abb. 1: Optionen für den Einsatz von Securitytokens

Zum andern stellt sich die Frage, wie feingranular die Rechte sein sollen, die sich aus einem Token ableiten. Im einfachsten Fall teilen sich alle Microfrontends ein Token, das Zugriff auf sämtliche Ressourcen im Backend gewährt. Auf diese Weise lässt sich Single-Sign-on für sämtliche Microfrontends sehr einfach umsetzen. Allerdings erhalten somit auch Angreifer, die das Token erfolgreich entwenden, Vollzugriff auf alle Services.

Alternativ dazu könnte der Identity Provider zunächst lediglich ein sehr feingranulares Token ausstellen, aus dem wenig Rechte hervorgehen. Braucht ein Microfrontend mehr Rechte, könnte es dieses feingranulare Token gegen ein Token für Services in seiner Subdomäne eintauschen.

Die nächsten Abschnitte gehen etwas genauer auf diese vier Varianten ein. Zuvor bietet der nachfolgende Abschnitt einen Überblick über die Nutzung von OAuth 2 und OpenID Connect zur Ausstellung von Tokens.

OAuth 2 und OpenID Connect für SPA

Große Geschäftsanwendungen verwalten in den wenigsten Fällen Ihre Benutzerkonten selbst, sondern interagieren mit existierenden Identity Providern wie Active Directory. Üblicherweise stellt der Identity Provider Securitytokens aus, mit denen der Client über die Identität des Benutzers informiert sowie Zugriff auf das Backend bekommt.

Damit der Identity Provider austauschbar bleibt, empfiehlt sich der Einsatz von standardisierten Protokollen. In der heutigen Welt der REST-artigen Services haben sich die Protokolle OAuth 2 und OpenID Connect durchgesetzt. Ersteres stellt dem Client ein Access-Token aus. Damit greift der Client im Namen des Benutzers auf das Backend zu. Das auf OAuth 2 aufbauende OpenID Connect versorgt den Client mit Informationen zum Benutzer. Das erfolgt über ein sogenanntes Identity-Token (ID-Token) und/oder bei Bedarf über eine HTTP-Anfrage bei einem normierten User-Info-Endpoint (Abb. 2). Der Client leitet hierzu den Benutzer zum Authorization-Server weiter. Dort muss sich der Benutzer zu erkennen geben, zum Beispiel mit Benutzername und Passwort oder Windows-Authentifizierung. Viele Authorization Server unterstützen auch verschiedene Arten der 2-Faktor-Authentifizierung.

steyer_authentifizierung_2.tif_fmt1.jpg
Abb. 2: OAuth 2 und OpenId Connect und Vogelperspektive

Nach erfolgreicher Authentifizierung muss der Benutzer dem Client explizit die Erlaubnis erteilen, in seinem Namen auf das Backend – von den Standards als Resource-Server bezeichnet – zugreifen zu dürfen. OAuth 2 spricht hierbei von Consent, also der Einwilligung oder Zustimmung des Benutzers. Gerade Geschäftsanwendungen überspringen diesen Punkt häufig. Hier geht man davon aus, dass das Unternehmen diese Einwilligung zum Zugriff auf Unternehmensressourcen bereits im Vorfeld erteilt hat.

Anschließend leitet der Authorization Server den Benutzer wieder zum Client um. Im Rahmen dieser Umleitung erhält der Client einen sogenannten Authorization-Code in Form eines URL-Parameters. Hat der Client Zugriff auf die HTTP-Payload, kann der Authorization Server den Parameter auch dort verstauen. Diesen Code tauscht der Client nun über eine verschlüsselte HTTPS-Verbindung gegen die Tokens.

ABTAUCHEN IM DEEP DIVE

Im Fortgeschrittenen Camp tauchen Sie ab unter die Oberfläche einer modernen Angular-Anwendung.
→ Nächster Termin: 13. - 15. Mai, München

Vom Implicit Flow zu Code-Flow und PKCE

Früher war es bei Single Page Applications üblich, die Tokens direkt in Form von URL-Parametern an den Client zu übersenden. Diesen vereinfachten Ablauf bezeichnen OAuth 2 und OpenID Connect als Implicit Flow. Damit wollte man den eingeschränkten Möglichkeiten in Browseranwendungen Rechnung tragen.

Aktuelle Best-Practice-Dokumente [7] raten davon jedoch ab, und mit der nächsten OAuth-Version 2.1 wird der Implicit Flow auch als veraltet gelten. Grund hierfür sind zahlreiche Attacken, die auf den Implicit Flow möglich sind. Allen voran Attacken, die darauf beruhen, dass URL-Parameter häufig sowohl in der Browser-History als auch in serverseitigen Logs gespeichert werden.

Stattdessen empfiehlt sich zumindest für neue Anwendungen der Einsatz des Authorization-Code-Flows. Dieser entspricht der zuvor erfolgten Beschreibung (Abb. 2) sowie der ursprünglichen Idee hinter OAuth 2.

Als Ergänzung empfehlen die aktuellen Best-Practice-Dokumente auch den Einsatz eines Proof Keys for Code Exchange [8], kurz PKCE (ausgesprochen „pixie“). Salopp gesprochen handelt es sich dabei um einen weiteren Code, der den Diebstahl des Access-Codes verhindern soll. Der Client generiert diesen Code und sendet ihn typischerweise als SHA256-Hash beim Initiieren des Flows in Form eines URL-Parameters an den Authorization Server. Beim Einlösen des Access-Codes via HTTPS präsentiert der Client den Code im Klartext und beweist somit, dass er den Flow initiiert hat und somit der rechtmäßige Besitzer des Access-Codes ist.

Clientseitiges Token-Handling

Die Implementierung der einzelnen durch OAuth 2 und OpenID Connect beschriebenen Flows in Browseranwendungen ist an und für sich kein Problem. Bibliotheken wie angular-oauth2-oidc [9] kümmern sich seit Jahren um diese Aufgabe und unterstützen mittlerweile sowohl den Implicit Flow als auch Code-Flow und PKCE (Listing 1).

Listing 1: Nutzung der Bibliothek Angular-oauth2-oidc in einer Angular-Anwendung

 

// Eckdaten zur Kommunikation mit dem Auth-Server bekannt geben
this.oauthService.configure(authCodeFlowConfig);
 
// Login starten (Umleitung auf Auth-Server)
this.oauthService.loadDiscoveryDocumentAndLogin();

 

Die eigentliche Herausforderung für die Anwendung entsteht jedoch erst nach dem Erhalt der Tokens: Die Tokens müssen irgendwo verstaut werden. Egal ob man sich für eine Eigenschaft im Programmcode, für den Session Storage oder den langlebigeren Local Storage entscheidet – sobald einem Angreifer eine XSS-Attacke gelingt, kann er die Tokens auslesen und im Namen des Benutzers auftreten.

Glücklicherweise gibt es mittlerweile einige Ansätze zum Abwehren von XSS-Attacken, und Securityaudits sind auch immer häufiger an der Tagesordnung. Um das Risiko bei einem Diebstahl weiter einzudämmen, ist es üblich, Tokens eine sehr kurze Lebenszeitspanne zu gewähren. Access-Tokens, die zum Beispiel nach 5 bis 20 Minuten ablaufen, sind keine Seltenheit. Das führt allerdings zum nächsten Problem: Der Client muss sich regelmäßig ein neues Token beim Identity Provider besorgen – und das möglichst ohne erneute Benutzerinteraktion.

Für diesen Token-Refresh gibt es leider keine gute Möglichkeit im Browser. Ein häufig anzutreffender Workaround sind HTTP-only-Cookies, die es dem Identity Provider ermöglichen, sich an den aktuellen Benutzer zu erinnern. Das Anfordern der neuen Tokens erfolgt bei diesem sogenannten Silent-Refresh häufig über ein verstecktes iFrame, sodass der Benutzer davon nichts mitbekommt und die aktuelle SPA geladen bleiben kann.

Leider stellt die Nutzung von Cookies in iframes auch ein mögliches Angriffsszenario dar, weswegen immer mehr Browser uns hier einen Strich durch die Rechnung machen. Eine andere Option ist der Einsatz von Refresh-Tokens, die es dem Client erlauben, neue Tokens ohne Benutzerinteraktion anzufordern. Allerdings müssen auch Refresh-Tokens irgendwo verstaut werden und ein Diebstahl solcher Tokens erlaubt es dem Angreifer, über lange Zeit hinweg im Namen des Benutzers aufzutreten.

Deswegen verbieten die Spezifikationen von OAuth 2 und OpenID Connect auch den Einsatz von Refresh-Tokens im Browser. Ein aktuelles Best-Practice-Dokument der OAuth-2.0-Arbeitsgruppe lockert diese Einschränkungen jedoch in bestimmten Fällen. Um die damit verbundenen Probleme jedoch prinzipiell zu lösen, spricht sich dasselbe Dokument aber auch sehr stark für die Handhabung von Tokens im Backend aus.

BRINGEN SIE LICHT INS ANGULAR-DUNKEL

Die ersten Schritte in Angular geht man am besten im Basic Camp.
→ Nächster Termin: 4. - 6. März, online

Serverseitiges Token-Handling

Um es Angreifern zu erschweren, Tokens zu entwenden, bietet es sich an, Tokens lediglich in einer serverseitigen Benutzersitzung zu verwalten. Eine sehr geradlinige Möglichkeit dazu ist der Einsatz eines Authentifizierungs-Gateways (Abb. 3).

steyer_authentifizierung_3.tif_fmt1.jpg
Abb. 3: Authentifizierungs-Gateway kümmert sich um OAuth 2 und OIDC

Diese Spielart, die ich in Ausgabe 2.2022 genauer beschrieben habe, erlaubt es u. a., die vielen Details des Token-Handlings in eine generische Lösung auszulagern. Die Idee ist, sämtliche Zugriffe auf die SPA, aber auch auf die eingebundenen APIs durch das Authentifizierungs-Gateway zu tunneln.

Beim ersten Zugriff leitet das Authentifizierungs-Gateway den Benutzer zur Authentifizierung an den Identity Provider weiter. Die so erhaltenen Tokens verstaut das Gateway in einer Benutzersitzung. Um sich an den Benutzer zu erinnern, stellt das Gateway ein HTTP-only-Cookie aus. Solche Cookies lassen sich nicht via JavaScript lesen und somit auch nicht bei XSS-Attacken entwenden.

Bei allen weiteren Zugriffen auf das API ergänzt das Gateway die HTTP-Anfrage um das Access-Token. Außerdem kümmert es sich bei Bedarf um den Token-Refresh mittels Refresh-Tokens. Dem Client stellt das Gateway einen einfachen Endpunkt zur Verfügung, der Informationen zum Benutzer liefert. Weitere einfache Endpunkte erlauben es dem Client, den Benutzer abzumelden bzw. erneut anzumelden. Somit ist der Implementierungsaufwand für den Client minimal (Listing 2).

Listing 2: Angular-Code zur Kommunikation mit Authentifizierungs-Gateway

 

@Injectable({
  providedIn: 'root'
})
export class AuthService {
 
  constructor(private http: HttpClient) { }
 
  loadUserInfo(): Observable<unknown> {
    return this.http.get<unknown>('/userinfo');
  }
 
  login(): void {
    location.href = '/login';
  }
 
  logout(): void {
    location.href = '/logout';
  }
 
}

 

Aber auch die einzelnen Services haben mit dem Token-Handling an sich sehr wenig zu tun. Sie prüfen lediglich die eingehenden Access-Tokens.

Tokenaustausch: Eine Hand wäscht die andere

Natürlich möchte sich der Benutzer nicht bei jedem Microfrontend unserer Gesamtlösung separat anmelden. Somit ist es verlockend, ein einziges Token zu teilen. Das heißt aber nicht, dass dieses Token Zugriff auf sämtliche Ressourcen aller Microfrontends erlauben soll. Ein solcher Master Key wäre natürlich ein gefundenes Fressen für Angreifer und das wollen wir ihnen nicht gönnen.

Deswegen wäre es sinnvoller, zunächst ein Token mit wenig Rechten anzufordern. Dieses Token kann dann jedes Microfrontend gegen ein weiteres Token tauschen, das lediglich Zugriff auf seine Domäne erlaubt. Abbildung 4 zeigt ein Authentifizierungs-Gateway, das so einen Tokenaustausch durchführt. Mittlerweile existieren gleich mehrere Standards, die in der Welt von OAuth 2 einen Tokenaustausch ermöglichen. Beispiele dafür sind [10] und [11].

steyer_authentifizierung_4.tif_fmt1.jpg
Abb. 4: Authentifizierungs-Gateway tauscht Token

Beispielanwendung

Wie die vorangegangenen Diskussionen zeigen, ist der Einsatz von serverseitigem Token-Handling und jeweils eines eigenen Tokens pro Domäne bzw. Microfrontend sehr verlockend. Genau dieses Szenario veranschaulicht die hier gezeigte Beispielanwendung. Es handelt sich dabei um eine Shell mit zwei Microfrontends – das eine dreht sich um Flugbuchungen und das andere verwaltet Passagiere (Abb. 5).

 

steyer_authentifizierung_5.tif_fmt1.jpgAbb. 5: Beispielanwendung

Das Beispiel tunnelt sämtliche Zugriffe auf die Microfrontends und ihre APIs durch ein Gateway. Beim ersten Zugriff fordert das Gateway ein ID-Token, ein sehr eingeschränktes Access-Token sowie ein Refresh-Token an. Sobald das Microfrontend für die Flugbuchung auf sein API zugreift, tauscht das Gateway sein Access-Token gegen ein Access-Token, das Zugriff auf dieses API gewährt. Analog geht das Gateway beim Zugriff auf das Passagier-API durch das Passagier-Microfrontend vor.

Keycloak in Container starten

Als Identity Provider nutze ich in diesem Beispiel die populäre und freie Identity-Lösung Keycloak [12] aus der Feder von Red Hat. Als dieser Text geschrieben wurde, lag Keycloak in der Version 17 vor. Diese Version erlaubt den Tokenaustausch in Anlehnung an den Standard [11]. Die Umsetzung in Version 17 ist jedoch noch eine Technology Preview, die sich beim Start des Servers über ein Kommandozeilenargument bzw. über eine entsprechende Einstellung in der Konfigurationsdatei aktivieren lässt. Für Entwicklungszwecke kann Keycloak in einem Docker-Container gestartet werden. Zur besseren Lesbarkeit wurde der entsprechende Aufruf in Listing 3 auf mehrere Zeilen umgebrochen. Lassen Sie uns einen genaueren Blick auf die einzelnen Parameter werfen:

Listing 3: Keycloak im Docker-Container starten

 

docker run 
  -p 7777:8080
  --volume /c/Users/Manfred/[…]/data:/opt/keycloak/data
  -e KEYCLOAK_ADMIN=admin
  -e KEYCLOAK_ADMIN_PASSWORD=admin
  quay.io/keycloak/keycloak:17.0.1 start-dev
  -Dkeycloak.profile.feature.token_exchange=enabled
  -Dkeycloak.profile.feature.admin_fine_grained_authz=enabled
  • Der Parameter -p mappt den Container-internen Port 8080, auf dem standardmäßig Keycloak läuft, auf den Port 7777 des Hostrechners. Keycloak ist also nach dem Start über http://localhost:7777 erreichbar.

  • –volume mappt den Ordner, in dem Keycloak seine Datenbank ablegt, auf einen lokalen Ordner. Somit bleiben die getätigten Einstellungen erhalten. Standardmäßig nutzt Keycloak eine datei-basierte H2-Datenbank [13]. Für Entwicklungszwecke ist das auch ausreichend. Für den Produktionseinsatz lassen sich zahlreiche andere Datenbanken konfigurieren.

  • Die mit -e definierten Umgebungsvariablen legen Benutzername und Passwort des eingerichteten Administrators fest.

  • Der Parameter start-dev startet Keycloak im Entwicklungsmodus.

  • Der Parameter -Dkeycloak.profile.feature.token_exchange aktiviert die Preview-Implementierung des Tokenaustauschs. Um Details des Tokenaustausches konfigurieren zu können, aktiviert -Dkeycloak.profile.feature.admin_fine_grained_authz u. a. die entsprechenden Optionen in der Administrationskonsole.

Clients und Tokenaustausch in Keycloak konfigurieren

Nach dem Start von Keycloak lassen sich einzelne Clients in der Browser-basierten Administrations-Konsole konfigurieren. Für das hier gezeigte Beispiel benötigen wir die drei Clients gateway, flight-api und passenger-api. Die Einstellungen für das Gateway finden sich in Tabelle 1.

Einstellung Wert
Client ID gateway
Access Type confitential
Valid Redirect URIs http://localhost:8080/signin-oidc

http://localhost:8080/signout-callback-oidc

Base URL http://localhost:8080
Advanced Settings | Access Token Lifespan 10 Minutes
Advanced Settings | Proof Key for Code Exchange Code Challenge Method S256

Tabelle 1: Keycloak-Konfiguration des Gateways

 

Alle hier gezeigten Einstellungen beschränken sich auf jene, die von den Standardeinstellungen abweichen und für das besprochene Szenario von Bedeutung sind. Besonders wichtig sind die Valid Redirect URIs. Nur an die hier angegebenen URLs macht Keycloak einen Redirect. Das soll verhindern, dass sich Angreifer in den Flow einklinken, um z. B. in den Besitz des Access-Codes zu kommen. Neben den hier gezeigten Einstellungen erhält das Gateway im Registerblatt Credentials ein Client-Secret. Mit diesem gibt es sich später Keycloak gegenüber zu erkennen.

Die Einstellungen für die beiden APIs fallen ein wenig einfacher aus (Tabelle 2). Der Access-Type bearer-only gibt an, dass diese Clients lediglich Tokens empfangen und somit keinen Flow zum Abrufen von Tokens via OAuth 2 bzw. OpenID Connect initiieren. Aus diesem Grund entfällt auch die Angabe von Redirect-URIs sowie die Aktivierung von PKCE. Auch ein Client-Secret ist für diese Clients nicht notwendig.

Einstellung Wert
Client ID flight-api bzw. passenger-api
Access Type bearer-only
Advanced Settings | Access Token Lifespan 10 Minutes

Tabelle 2: Keycloak-Konfiguration des Flight-API und Passenger-API

Nun müssen wir es dem Gateway noch erlauben, erhaltene Access-Tokens gegen Access-Tokens für den Zugriff auf die beiden APIs einzutauschen. Dazu ist die Konfiguration des flight-api und des passenger-api zu erweitern. Die nötigen Einstellungen finden sich im Registerblatt Permissions, das in Keycloak 17 nur sichtbar ist, wenn Sie die oben diskutierte Preview-Implementierung aktivieren (Abb. 6).

steyer_authentifizierung_6.tif_fmt1.jpgAbb. 6: Permissions für APIs

Hier ist die Option Permissions Enabled zu aktivieren. Ein Klick auf den Link token-exchange führt zu einer Seite, die das Einrichten einer Policy für den Tokenaustausch erlaubt. Für das hier beschriebene Vorhaben ist eine Policy einzurichten, die einen Tokenaustausch erlaubt. Dabei handelt es sich um eine sogenannte Client-Policy. Um sie einzurichten, ist im Drop-down-Feld auf der rechten Seite der Eintrag Client auszuwählen (Abb. 7). Daraufhin erscheint ein Detaildialog (Abb. 8). Die Policy erhält einen beliebigen eindeutigen Namen. Außerdem ist im Drop-down-Feld der Client gateway auszuwählen.

steyer_authentifizierung_7.tif_fmt1.jpgAbb. 7: Client-Policy
steyer_authentifizierung_8.tif_fmt1.jpgAbb. 8: Policy für Tokenaustausch

Wechseln Sie nach dem Speichern der Policy auf die vorherige Seite und stellen Sie sicher, dass die neue Policy unter Apply Policy aufscheint (Abb. 7). Dazu müssen Sie ggf. die Policy aus dem Drop-down-Feld auswählen, nachdem Sie dort den Anfangsbuchstaben Ihres Namens erfasst haben. Diese Einstellung ist für beide APIs vorzunehmen.

Neben den Clients benötigen wir noch den einen oder anderen Benutzer, der sich auch in der Administrationskonsole einrichten lässt. Es empfiehlt sich, auch optionale Felder wie Vorname, Nachname oder E-Mail-Adresse auszufüllen. Wir werden diese Werte später im ausgestellten ID-Token wiederfinden. Ähnlich wie bei den Clients existiert für Benutzer ein eigenes Registerblatt Credentials. Hier lassen sich Passwörter hinterlegen (Kasten: „Demo: Keycloak in der Cloud“).

Demo: Keycloak in der Cloud

Alternativ können wir zum Testen der hier diskutierten Lösung auch einen Keycloak-Server in der Cloud betreiben. Sie finden alle nötigen Eckdaten in dem Abschnitt dieses Artikels, in dem es um die Konfiguration und den Start des Gateways geht.

Implementierung mit YARP und ASP.NET Core

Die Umsetzung des Gateways erfolgt mit YARP. Die Abkürzung YARP steht für Yet Another Reverse Proxy [14]. Das Besondere daran ist, dass Microsoft YARP als Middleware für ASP.NET Core entwickelt hat. Das bedeutet, dass man nicht nur einen vollwertigen Reverse Proxy mit den typischen Möglichkeiten wie Lastausgleich, Monitoring und Health-Checks bekommt, sondern dass sich auch sämtliche existierende Middlewarelösungen für ASP.NET Core sowie eigene Erweiterungen nutzen lassen. Zum Beispiel lässt sich Microsofts OpenID-Connect-Implementierung für ASP. NET Core mit ein paar Zeilen Code hinzufügen.

Das gesamte Gateway ist somit vereinfacht ausgedrückt eine Ansammlung von Standardmiddlewarekomponenten aus der Feder von Microsoft sowie ein paar Erweiterungen, die sich unter anderem um den Tokenaustausch sowie das Anhängen von Tokens an API-Aufrufe kümmern.

Wer nichts mit ASP.NET Core am Hut hat, kann die gesamte Lösung auch als Blackbox betrachten und sie in einem Container laufen lassen. Ein dockerfile hierfür liegt dem Beispiel unter [1] bei. Außerdem findet sich dort auch eine docker-compose.yml, die das Austauschen der Gateway-Konfiguration vereinfacht.

Konfiguration des Gateways

Die Konfiguration des Gateways befindet sich in der Datei appsettings.json (Listing 4).

Listing 4: YARP und Middlewarekomponenten konfigurieren

 

"Gateway": {
  "SessionTimeoutInMin": "60",
  "Url": "http://localhost:8080",
  "TokenExchangeStrategy": "default"
},
"Apis": [
  {
    "ApiPath": "/flight-api/",
    "ApiAudience": "flight-api"
  },
  {
    "ApiPath": "/passenger-api/",
    "ApiAudience": "passenger-api"
  }
],
"OpenIdConnect": {
  "Authority": "http://164.92.183.220:7777/realms/master",
  "ClientId": "gateway",
  "ClientSecret": "WmsokbzFvqWRWIijKLgLktMFVnQR1TUn",
  "Scopes": "openid profile email offline_access"
},

 

Da sich derzeit die verschiedenen Identity-Lösungen auf verschiedene Standards für den Tokenaustausch stützen und diese teilweise auch lose interpretieren, nutzt das Gateway hierfür einen austauschbaren Service. Dieser lässt sich über die Eigenschaft TokenExchangeStrategy konfigurieren. Die hier verwendete Einstellung default nutzt das unter [2] beschriebene Verfahren, das heute schon von Keycloak in seinen Grundzügen unterstützt wird. Dieses Verfahren scheint sich für das verfolgte Vorhaben am besten zu eignen. Aus diesem Grund ist davon auszugehen, dass es auch andere Produkte früher oder später unterstützen werden.

Weitere mögliche Werte sind AzureAd für die Interpretation von [3] durch Azure Active Directory und none für Fälle, in denen kein Tokenaustausch stattfinden soll bzw. die genutzte Identity-Lösung diese Möglichkeit gar nicht bietet.

Der Abschnitt Apis definiert für alle Pfade, die YARP auf ein API weiterleitet, die Client-ID des jeweiligen API als ApiAudience. Diese Information gibt das Gateway beim Tokenaustausch an. Das Ergebnis ist ein neues Access-Token, das Zugriff auf das jeweilige API erlaubt.

Die verwendete Keycloak-Version erlaubt noch nicht die Angabe von Scopes für dieses neue Access-Token. Das soll sich jedoch mit einer künftigen Version ändern. Ist das mal der Fall, können Sie dem Gateway pro API den gewünschten Scope über die Eigenschaft ApiScope mitteilen:

 

"ApiPath": "/flight-api/",
  "ApiAudience": "flight-api",
  "ApiScope": "read write delete"

 

Unter OpenIdConnect finden sich die Einstellungen für die OpenID-Connect-Middleware. Als Authority kommt der URL der verwendeten Keycloak-Instanz zum Einsatz. Das Beispiel verweist auf eine Keycloak-Instanz, die wir zum Testen über die Cloud anbieten. Sowohl die hier hinterlegte ClientId als auch das ClientSecret wurden in Keycloak für das Gateway konfiguriert. Der Scope drückt die Rechte aus, die der Client im Namen des Benutzers wahrnehmen möchte. Mit den Einträgen openid profile und email drückt der Client aus, dass er Benutzerdaten via OpenID Connect lesen möchte, und offline_access führt zur Ausstellung eines Refresh-Tokens.

Die appsettings.json enthält auch Konfigurationseinträge für YARP. Darunter finden sich Routen, die auf einzelne APIs weitergeleitet werden (Listing 5).

Listing 5: Routen und Cluster

 

"ReverseProxy": {
  "Routes": {
    "flightApiRoute": {
      "ClusterId": "flightApiCluster",
      "AuthorizationPolicy": "authPolicy",
      "Match": {
        "Path": "flight-api/{**remainder}"
      },
      "Transforms": [
        {
          "PathPattern": "/api/{**remainder}"
        }
      ]
    },
    [...]
  },
  "Clusters": {
    "flightApiCluster": {
      "Destinations": {
        "destination1": {
          "Address": "http://demo.angulararchitects.io"
        }
      }
    },
    [...]
  }
}

 

Der gezeigte Fall definiert eine Route für alle Pfade, die mit flight-api beginnen. Die Einstellung transforms ändert dieses Präfix auf den vom Flight API tatsächlich verwendeten Präfix api. Anschließend erfolgt die Weiterleitung an den weiter unten definierten Cluster, der wiederum auf die Adresse des Flight API verweist. Als Cluster bezeichnet YARP eine Menge von Adressen, hinter denen sich jeweils eine Instanz desselben Diensts befindet. Auf diese Weise lässt sich Load Balancing realisieren.

Gateways starten und Konfiguration austauschen

Standardmäßig nutzt das Gateway die Konfiguration in der Datei appsettings.json. Allerdings lässt sich eine davon abweichende Konfigurationsdatei beim Start sowohl über einen Kommandozeilenparameter als auch über eine Umgebungsvariable angeben. Ersteres kann bei lokalen Tests nützlich sein. Der folgende Aufruf nutzt zum Beispiel die beiliegende Keycloak-Demokonfiguration im Ordner conf:

dotnet run conf/appsettings.keycloak.json

Vor dem ersten Start müssen auch noch die Abhängigkeiten geladen werden:

dotnet restore

Für Testzwecke können Sie das Benutzerkonto jane, dessen Passwort ebenfalls jane lautet, verwenden. Weitere Demokonfigurationen, u. a. für Azure Active Directory, Auth0 oder Identity Server, finden sich auch in diesem Ordner. Alternativ dazu lässt sich die zu nutzende Konfigurationsdatei über die Umgebungsvariable GATEWAY_CONFIG angeben. Diese Möglichkeit ist u. a. beim Betrieb in einem Container interessant:

docker build -t auth-gateway .
 
docker run 
  -it --rm -p 8080:8080 
  -e GATEWAY_CONFIG=conf/appsettings.keycloak.json 
  --name auth-gateway auth-gateway 

Zur besseren Lesbarkeit wurde der Aufruf von docker run hier wieder umgebrochen. Etwas komfortabler gestaltet sich die Nutzung von Docker Compose. Die dafür beiliegende docker-compose.yml beinhaltet bereits alle nötigen Informationen, wie die zu nutzende Konfigurationsdatei und die Ports, die beim Aufruf von docker run von Hand angegeben werden müssen. Außerdem bildet sie den lokalen Ordner conf auf den entsprechenden Ordner im Container ab. Nach einer Änderung einer der Konfigurationsdateien ist ein Neustart des Gateways trotzdem notwendig, jedoch muss der Container nicht neu gebaut werden. Um das Gateway mit Docker Compose zu starten, nutzen Sie die folgende Anweisung:

docker compose up 

Ein Blick hinter die Kulissen des Gateways

Das YARP-basierte Gateway ist sehr leichtgewichtig aufgebaut. Im Wesentlichen registriert es lediglich Services bei einem DI-Container sowie Middlewarekomponenten, die ihren Teil zu allen Anfragen und Antworten beitragen (Listing 6).

Listing 6: YARP und Middlewarekomponenten konfigurieren

var builder = WebApplication.CreateBuilder(args);
 
//
// 1. Register Services for DI
//
 
builder.Services.AddReverseProxy()
  .LoadFromConfig(builder.Configuration.GetSection("ReverseProxy"));
 
builder.Services.AddAuthentication([...])
  .AddCookie([...])
  .AddSession([...])
  [...]
  .AddOpenIdConnect(options => { [...] });
 
 
var app = builder.Build();
 
//
// 2. Add Middleware Components
//
 
[...]
app.UseAuthentication();
app.UseAuthorization();
[...]
app.MapReverseProxy([…]);
[...]
 
//
// 3. Start Gateway
//
 
app.Run("http://+:8080");

Die von YARP benötigten Services registriert die Erweiterungsmethode AddReverseProxy beim DI-Container. Die zu nutzenden Einstellungen entnimmt das Beispiel der Sektion ReverseProxy in der Konfigurationsdatei.

Services für weitere im Lieferumfang von ASP.NET Core enthaltene Features fügen die Erweiterungsmethoden AddAuthenticationAddCookieAddSession und AddOpenIdConnect hinzu. Diese Methode nehmen zahlreiche hier nicht abgebildete Konfigurationsparameter. Darunter befinden sich zum Beispiel die Eckdaten für die Kommunikation mit dem Authorization Server.

Nach dem Aufruf der Methode Build kreiert das Beispiel eine Pipeline mit Middlewarekomponenten, die ASP.NET Core für jede ausgehende Anfrage und eingehende Antwort durchläuft. Die Erweiterungsmethode MapReverseProxy fügt am Ende der Pipeline die Middlewarekomponente für YARP hinzu. Die Parameter dieser Methode bieten auch Einsprungpunkte für Erweiterungen. Auf deren Basis stellt das Gateway sicher, dass zum richtigen Zeitpunkt ein Tokenaustausch stattfindet bzw. die entsprechenden Access-Tokens an API-Anfragen angehängt werden. Die Methode Run startet schlussendlich das Gateway.

Um den programmatischen Einsatz des Gateways zu vereinfachen, versteckt die hier besprochene Implementierung viele dieser Aufrufe hinter eigenen Erweiterungsmethoden (Listing 7).

Listing 7: Erweiterungsmethoden des Gateways

var builder = WebApplication.CreateBuilder(args);
 
[...]
 
//
// 1. Register Services for DI
//
 
builder.AddGateway(config, disco);
 
var app = builder.Build();
 
//
// 2. Add Middleware Components
//
 
app.UseGateway();
 
//
// 3. Start Gateway
//
 
if (string.IsNullOrEmpty(config.Url)) {
  app.Run();
}
else {
  app.Run(config.Url);
}

Einsatz zur Laufzeit

Das Logging des Gateways erlaubt es, die implementierte Funktionsweise nachzuvollziehen. Nach dem Start des Gateways bietet es Zugriff auf die Shell, die die einzelnen Microfrontends lädt. Entsprechend der gezeigten Konfiguration ist das Gateway über http://localhost:8080 erreichbar.

Die einzelnen angeforderten Access-Tokens protokolliert das Gateway im Debugmodus auf der Konsole. Es liegt auf der Hand, dass diese Funktion in der Produktion nicht verwendet werden soll. Bei den Tokens handelt es sich um JSON Web Tokens (JWT), die sich z. B. unter https://jwt.io dekodieren und inspizieren lassen. Dabei fällt auf, dass das erste angeforderte Access-Token lediglich Zugriff auf das Benutzerkonto gibt:

{
  "aud": "account",
  [...]
}

Beim Zugriff auf die APIs erfolgt der Tokenaustausch. Die so erhaltenen Tokens erlauben zusätzlich den Zugriff auf das jeweilige API:

{
  "aud": [
    "account",
    "flight-api"
  ],
  [...]
}

Fazit

Leider ist der Browser kein sicherer Ort für das Verstauen von Tokens. Auch für den Token-Refresh existieren clientseitig keine wirklich guten Optionen. Deswegen empfiehlt mittlerweile auch die OAuth-2-Arbeitsgruppe, das Handling der Tokens auf die Serverseite zu verlagern.

Dank eines Authentifizierungs-Gateways lässt sich diese Idee generisch implementieren, sodass sich weder der Client noch das Backend umfangreich damit belasten müssen.

Bei Microfrontends und großen Modulithen ist es allerdings zu wenig, sich nur ein einziges Token zu teilen. Vielmehr braucht man hier mehrere feingranulare. Deswegen empfiehlt es sich, zunächst nur ein Token mit sehr eingeschränkten Rechten anzufordern. Die einzelnen Microfrontends bzw. Frontend-Module können dieses Token ohne weitere Benutzerinteraktion gegen ein weiteres Token eintauschen, das den Zugriff auf ihr API bzw. ihre Domäne gewährt. Auch diese Aufgabe lässt sich in einem Authentifizierungs-Gateways automatisieren.