Authentifizierung im Frontend umstellen

Was du in deiner Angular- oder React-App ändern musst, wenn die Authentifizierung statt über keycloak-js jetzt über das Portal-Cookie läuft.

18. August 2026 Christian Engel E-Mail schreiben In Teams chatten

Dieser Guide beschreibt die technischen Änderungen im Frontend während Phase 3. Der übergeordnete Ablauf und die Voraussetzungen für das Deployment sind unter Portal-Integration in den Helm Values deklarieren beschrieben.

Zielzustand

Bisher hat das Frontend einen JWT selbst beschafft oder vom Host erhalten, an Backend-Requests angehängt und gegebenenfalls an eingebettete Microfrontends weitergegeben.

Im Zielzustand verarbeitet das Frontend keinen JWT mehr. Die Authentifizierung erfolgt über das Cookie der Portal-Domain. Bei Requests, die über das Auth-Gateway an ein Backend geleitet werden, ergänzt das Gateway den Authorization-Header.

Was konkret wegfällt:

  • keycloak-js und dessen Initialisierung
  • Code, der ein Token vom Host empfängt
  • Code, der ein Token explizit an Backend-Requests anhängt (Interceptoren, Fetch-Wrapper)
  • Code, der Informationen aus dem JWT ausliest
  • Hardcodierte oder env-basierte Backend-URLs

Was neu hinzukommt:

  • Jeder Backend-Request muss das Portal-Cookie mitschicken
  • Angaben zum angemeldeten Nutzer werden mit getSessionInfo() im Frontend abgerufen
  • Backend-URLs werden zur Laufzeit aus der Portal-Domain abgeleitet

Token-Verarbeitung entfernen

keycloak-js

Falls dein Frontend keycloak-js selbst initialisiert:

import Keycloak from 'keycloak-js';

const keycloak = new Keycloak({ url: '...', realm: '...', clientId: '...' });
await keycloak.init({ onLoad: 'login-required' });

Die Initialisierung entfällt vollständig. Beim Laden des Microfrontends besteht bereits eine Sitzung im Portal.

Angular: Entferne KeycloakService und KeycloakAngularModule aus dem AppModule sowie die Verwendung von KeycloakService in Services und Guards.

React: Entferne den ReactKeycloakProvider und alle useKeycloak()-Aufrufe.

Token vom Host empfangen

Entferne die Übernahme eines JWT per postMessage, Attribut oder einer anderen Schnittstelle des Hosts. Im Zielzustand empfängt und verteilt das Microfrontend keinen JWT mehr.

Authorization-Header entfernen

Angular – HttpInterceptor:

@Injectable()
export class AuthInterceptor implements HttpInterceptor {
	intercept(req: HttpRequest<unknown>, next: HttpHandler) {
		const cloned = req.clone({
			headers: req.headers.set('Authorization', `Bearer ${this.token}`)
		});
		return next.handle(cloned);
	}
}

Entferne den Interceptor aus HTTP_INTERCEPTORS in deinem AppModule oder provideHttpClient()-Aufruf.

React – fetch / axios:

fetch(url, {
	headers: { Authorization: `Bearer ${token}` }
});

Entferne den explizit gesetzten Authorization-Header. Das Auth-Gateway ergänzt ihn beim Weiterleiten des Requests an das Backend.

Das Frontend läuft im Portal beispielsweise unter mein-mfe.<portal-domain>, das Backend unter mein-backend.<portal-domain>. Für den Browser sind das unterschiedliche Origins. Damit das Cookie bei einem Cross-Origin-Request mitgesendet wird, muss der Request Credentials einschließen.

Fehlt diese Angabe, erreicht der Request das Gateway ohne Session-Cookie und das Backend antwortet mit Status 401.

Das gilt für jeden Request an ein Backend, nicht nur für den ersten:

// fetch
fetch(url, { credentials: 'include' });

// axios
axios.get(url, { withCredentials: true });
// Angular – einmalig global:
provideHttpClient(withInterceptors([(req, next) => next(req.clone({ withCredentials: true }))]));

Konfiguriere withCredentials in Angular zentral, damit es für alle Backend-Requests gilt.

Angaben zum Nutzer

Hat das Frontend bisher Informationen wie Name oder Land aus dem JWT gelesen, rufe diese künftig mit getSessionInfo() aus @dvag/mfe-toolkit ab. Erstelle dafür keine eigene Backend-Route. Dem Backend liegen diese Informationen nicht vor.

import { getSessionInfo } from '@dvag/mfe-toolkit/url';

const { name, firstName, lastName, land } = await getSessionInfo();

Die Funktion ist ab Version 0.4.0 verfügbar und lädt die Session-Informationen über die Portal-Domain. Das zugrunde liegende Cookie bleibt HttpOnly; das Frontend verarbeitet weiterhin keinen JWT.

Backend-URLs zur Laufzeit ableiten

Das Backend läuft im Portal unter <name>.<portal-domain>. Da die Domain vom jeweiligen Portal abhängt, kann sie nicht zur Build-Zeit festgelegt werden.

Das Paket @dvag/mfe-toolkit aus dvag/frontend-boilerplate stellt dafür getPortalUrl bereit:

import { getPortalUrl } from '@dvag/mfe-toolkit/url';

const apiBase = getPortalUrl('mein-backend');

Der Parameter ist der name aus dem portalIntegration-Block deines Backends in den Helm Values:

portalIntegration:
  name: mein-backend
  type: backend

Verwende diese Funktion im Portal anstelle buildzeitabhängiger API-URLs wie process.env.API_URL oder environment.apiUrl. Das Toolkit leitet die vollständige URL aus der aktuellen window.location ab.

Lokal entwickeln ohne Portal

Für die lokale Entwicklung übernimmt @dvag/dev-auth-proxy die Aufgabe des Auth-Gateways. Der Proxy führt den Keycloak-Login im Browser durch, aktualisiert den Access-Token und leitet Requests mit Authorization-Header an das Backend weiter. Das Frontend bleibt dabei tokenfrei.

Lege im Root-Verzeichnis deines Frontends eine dev-auth-proxy.json an:

{
	"entwicklung": {
		"keycloakClientId": "mein-keycloak-client",
		"upstreamTarget": "https://mein-backend.entwicklung.dvag/"
	}
}

Starte zuerst den Proxy:

npx @dvag/dev-auth-proxy entwicklung

Der Proxy verwendet Port 4200 für den Login-Callback und gibt ihn nach dem Login wieder frei. Starte den Frontend-Dev-Server deshalb erst nach Abschluss des Logins. In der lokalen Konfiguration gehen Backend-Requests an http://localhost:8080; im Portal wird weiterhin die mit getPortalUrl() abgeleitete URL verwendet.

Auch lokal müssen Requests credentials: 'include' beziehungsweise withCredentials: true verwenden. Fehlt das beim Login gesetzte HttpOnly-Cookie, antwortet der Proxy mit Status 400. Damit entspricht der relevante Teil des lokalen Request-Flows dem Verhalten im Portal.

getSessionInfo() verwendet auf localhost automatisch die /session-info-Route des Dev-Auth-Proxys auf Port 8079.

Für mehrere Backends kann upstreamTarget als Liste konfiguriert werden. Die Ziele sind in derselben Reihenfolge über die Ports 8080, 8081 und folgende erreichbar.

E2E-Tests mit Playwright

E2E-Tests dürfen den JWT künftig ebenfalls nicht selbst beschaffen oder als Authorization-Header an Requests anhängen:

formData.append('client_id', config.AuthentificationClientID);
formData.append('username', vbnummer);
formData.append('password', vbkennwort);
formData.append('grant_type', 'password');

const headers = {
	'Content-Type': 'application/json',
	Authorization: `Bearer ${access_token}`
};

Stattdessen startet createAuthProxy den Auth-Proxy im globalSetup. Playwright öffnet die von createAuthProxy bereitgestellte startUrl und führt den Keycloak-Login mit den vorhandenen Page Objects aus. Der dabei gespeicherte storageState enthält das Cookie für localhost und wird für Browser- und request-Kontexte konfiguriert:

// playwright.config.ts
export default defineConfig<TestOptions>({
	globalSetup: './global-setup.ts',
	use: {
		storageState: STATE.VB_PWTESTUSER_AUTH
	}
});

Der Proxy muss während des gesamten Testlaufs erreichbar bleiben und anschließend beendet werden. API-Requests verwenden die lokale Proxy-URL und setzen keinen Authorization-Header:

const url = `http://localhost:8080/kunden/${kundennummer}/ausgaben`;
const headers = { 'Content-Type': 'application/json' };

const response = await this.requestSender.sendRequest('POST', url, headers, body);

Fehlt das Cookie, antwortet der Proxy mit Status 400.

Confidential Client in GitHub Actions

Für Tests gegen Umgebungen, in denen der Public Client localhost nicht als Redirect-URI verwenden darf, unterstützt @dvag/dev-auth-proxy ab Version 1.3.0 einen Confidential Client. Der Login läuft weiterhin als Authorization Code Flow mit PKCE über den Testnutzer. Der Proxy authentifiziert sich beim Code-Tausch und bei jedem Token-Refresh zusätzlich mit dem Client-Secret. Es handelt sich nicht um einen Client-Credentials-Flow.

Hinterlege das Client-Secret als GitHub Actions Secret und stelle es nur dem Playwright-Schritt als Umgebungsvariable bereit:

- name: Run Playwright tests
  run: npx playwright test
  env:
    DEV_AUTH_PROXY_CLIENT_SECRET: ${{ secrets.DEV_AUTH_PROXY_CLIENT_SECRET }}

Lies das Secret im globalSetup ein und übergib es an createAuthProxy:

import { createAuthProxy } from '@dvag/dev-auth-proxy';

const clientSecret = process.env.DEV_AUTH_PROXY_CLIENT_SECRET;
if (!clientSecret) {
	throw new Error('DEV_AUTH_PROXY_CLIENT_SECRET is required');
}

const proxy = await createAuthProxy({
	environment: 'abnahme',
	clientId: 'mein-confidential-client',
	clientSecret,
	upstreamTarget: 'https://mein-backend.abnahme.dvag/'
});

Beim Confidential Client muss http://localhost:<callback-port> als gültige Redirect-URI hinterlegt sein. Proxy und Playwright-Browser müssen auf demselben GitHub Actions Runner beziehungsweise im selben Netzwerk-Namespace laufen.

Das Secret gehört nicht in dev-auth-proxy.json, den Quellcode, CLI-Argumente, Logs oder Testartefakte. Ohne clientSecret verwendet der Proxy weiterhin den bisherigen Public-Client-Flow, beispielsweise für Entwicklung und Integration.

Checkliste

  • keycloak-js und dessen Initialisierung entfernt
  • Token-Empfang vom Host-Frame entfernt
  • Authorization-Header-Interceptor / -Wrapper entfernt
  • Alle Backend-Requests verwenden credentials: 'include' / withCredentials: true
  • Zugriffe auf JWT-Claims im Frontend durch getSessionInfo() ersetzt; keine eigene Backend-Route erstellt
  • Backend-URLs werden zur Laufzeit abgeleitet, nicht mehr hardcodiert
  • Lokale Entwicklung verwendet @dvag/dev-auth-proxy; das Frontend enthält auch lokal kein Token-Handling
  • E2E-Tests verwenden den Auth-Proxy und beschaffen keinen JWT selbst
  • Falls E2E-Tests einen Confidential Client verwenden: @dvag/dev-auth-proxy ist mindestens Version 1.3.0 und das Client-Secret kommt aus der CI-Secret-Verwaltung