Multi-Audience Auth0 Authentication in Micro Frontends: Der Token-Management-Albtraum, den wir gelöst haben
Praxiserprobte Implementierung von Auth0 Multi-Audience Authentication über Micro Frontends, Token-Management-Strategien und Silent Authentication in React Native mit WebView-basierten Micro Frontends
Vor drei Monaten wurde unsere "einfache" Micro-Frontend-Migration zu einem Authentication-Albtraum. Fünf verschiedene Teams, acht Micro Frontends, drei APIs, und irgendwie mussten wir sie alle über Auth0 authentifizieren, ohne dass User sich mehrmals einloggen. Der Clou? Es musste auch in unserer React Native App funktionieren, die diese Micro Frontends in WebViews einbettet. So haben wir dieses Chaos entwirrt und ein Token-Management-System gebaut, das tatsächlich funktioniert.
Das Problem: Multiple Audiences, Ein Login#
Stell dir diese Architektur vor:
Loading diagram...
Jede API braucht eine andere Audience im JWT Token. Auth0s Default Flow? Eine Audience pro Authentication. Unsere User müssten sich dreimal einloggen. Geht nicht.
Die Lösung: Multi-Audience Token Management#
Nach zwei Wochen Auth0-Dokumentation und mehreren Debugging-Sessions um 2 Uhr morgens, hier ist was wirklich funktioniert:
1. Die Token Manager Architektur#
// token-manager.ts - Das Gehirn unseres Auth-Systems
interface TokenSet {
accessToken: string;
idToken: string;
refreshToken: string;
expiresAt: number;
audience: string;
scope: string;
}
class MultiAudienceTokenManager {
private tokens: Map<string, TokenSet> = new Map();
private primaryRefreshToken: string | null = null;
private auth0Client: Auth0Client;
constructor(config: Auth0Config) {
this.auth0Client = new Auth0Client({
domain: config.domain,
clientId: config.clientId,
cacheLocation: 'memory', // Kritisch für Micro Frontends
useRefreshTokens: true,
authorizeTimeoutInSeconds: 60
});
}
async loginWithMultipleAudiences(audiences: string[]): Promise<void> {
// Schritt 1: Login mit primärer Audience (enthält alle Scopes)
const primaryAudience = audiences[0];
const allScopes = this.getAllRequiredScopes();
const result = await this.auth0Client.loginWithRedirect({
audience: primaryAudience,
scope: allScopes,
redirect_uri: window.location.origin
});
// Nach Redirect Callback
const tokens = await this.auth0Client.handleRedirectCallback();
this.primaryRefreshToken = tokens.refreshToken;
// Primären Token speichern
this.storeToken(primaryAudience, tokens);
// Schritt 2: Tokens für andere Audiences still holen
for (const audience of audiences.slice(1)) {
await this.getTokenForAudience(audience);
}
}
async getTokenForAudience(audience: string): Promise<string> {
// Zuerst Cache checken
const cached = this.tokens.get(audience);
if (cached && cached.expiresAt > Date.now()) {
return cached.accessToken;
}
try {
// Zuerst Silent Authentication versuchen
const token = await this.auth0Client.getTokenSilently({
audience: audience,
scope: this.getScopeForAudience(audience),
cacheMode: 'off' // Frischen Token erzwingen
});
this.storeToken(audience, {
accessToken: token,
expiresAt: Date.now() + 3600000, // 1 Stunde
audience: audience,
scope: this.getScopeForAudience(audience)
});
return token;
} catch (error) {
// Wenn Silent Auth fehlschlägt, Refresh Token verwenden
if (this.primaryRefreshToken) {
return this.refreshTokenForAudience(audience);
}
throw error;
}
}
private async refreshTokenForAudience(audience: string): Promise<string> {
// Auth0 Token Endpoint mit Refresh Token Grant
const response = await fetch(`https://${this.auth0Client.domain}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: this.auth0Client.clientId,
refresh_token: this.primaryRefreshToken,
audience: audience,
scope: this.getScopeForAudience(audience)
})
});
const data = await response.json();
this.storeToken(audience, {
accessToken: data.access_token,
idToken: data.id_token,
expiresAt: Date.now() + (data.expires_in * 1000),
audience: audience,
scope: data.scope
});
return data.access_token;
}
}
2. Cross-Domain Token Sharing für Micro Frontends#
Die größte Herausforderung: Tokens über verschiedene Subdomains teilen. Unsere getestete Lösung:
// shared-auth-context.tsx - Von allen Micro Frontends verwendet
import { createContext, useContext, useEffect, useState } from 'react';
interface SharedAuthState {
isAuthenticated: boolean;
tokens: Map<string, string>;
user: any;
}
const SharedAuthContext = createContext<SharedAuthState | null>(null);
// Broadcast Channel für Cross-Tab/Cross-Iframe Kommunikation
const authChannel = new BroadcastChannel('auth-sync');
export function SharedAuthProvider({ children, audience }: Props) {
const [authState, setAuthState] = useState<SharedAuthState>();
const [tokenManager] = useState(() => new MultiAudienceTokenManager());
useEffect(() => {
// Auf Auth-Updates von anderen Micro Frontends hören
authChannel.onmessage = (event) => {
if (event.data.type === 'AUTH_UPDATE') {
setAuthState(event.data.payload);
}
};
// Prüfen ob wir über Shared Storage authentifiziert sind
checkSharedAuthentication();
}, []);
const checkSharedAuthentication = async () => {
// Mehrere Storage-Strategien versuchen
// Strategie 1: Shared localStorage via iframe postMessage
const sharedToken = await getTokenFromShell();
// Strategie 2: Server-side Session Check
if (!sharedToken) {
const session = await checkServerSession();
if (session) {
await silentAuthentication();
}
}
// Strategie 3: Auth0 Session Check
if (!sharedToken) {
const auth0Session = await checkAuth0Session();
if (auth0Session) {
await getTokenSilently();
}
}
};
const getTokenFromShell = (): Promise<string | null> => {
return new Promise((resolve) => {
// Nachricht an Shell Application senden
window.parent.postMessage(
{ type: 'GET_TOKEN', audience },
'https://auth.myapp.com'
);
// Auf Antwort hören
const handler = (event: MessageEvent) => {
if (event.origin !== 'https://auth.myapp.com') return;
if (event.data.type === 'TOKEN_RESPONSE') {
window.removeEventListener('message', handler);
resolve(event.data.token);
}
};
window.addEventListener('message', handler);
// Timeout nach 1 Sekunde
setTimeout(() => {
window.removeEventListener('message', handler);
resolve(null);
}, 1000);
});
};
return (
<SharedAuthContext.Provider value={authState}>
{children}
</SharedAuthContext.Provider>
);
}
3. Die Shell Application - Authentication orchestrieren#
// shell-application.tsx - Der Authentication Orchestrator
class ShellAuthOrchestrator {
private microFrontends: Map<string, MicroFrontendConfig> = new Map();
private tokenManager: MultiAudienceTokenManager;
private sessionManager: SessionManager;
async initialize() {
// Alle Micro Frontends und ihre benötigten Audiences registrieren
this.registerMicroFrontends([
{
name: 'billing',
url: 'https://billing.myapp.com',
audience: 'https://api.myapp.com/billing',
scopes: ['read:invoices', 'write:payments']
},
{
name: 'dashboard',
url: 'https://dashboard.myapp.com',
audience: 'https://api.myapp.com/core',
scopes: ['read:profile', 'read:data']
},
{
name: 'analytics',
url: 'https://analytics.myapp.com',
audience: 'https://api.myapp.com/analytics',
scopes: ['read:reports', 'read:metrics']
}
]);
// Message Handler für Micro Frontend Token Requests einrichten
window.addEventListener('message', this.handleTokenRequest);
// Authentication Status prüfen
await this.checkAuthentication();
}
private handleTokenRequest = async (event: MessageEvent) => {
// Origin validieren
const mfe = this.getMicroFrontendByOrigin(event.origin);
if (!mfe) return;
if (event.data.type === 'GET_TOKEN') {
const token = await this.tokenManager.getTokenForAudience(
event.data.audience
);
// Token zurück zum anfragenden Micro Frontend senden
event.source?.postMessage(
{
type: 'TOKEN_RESPONSE',
token: token,
audience: event.data.audience
},
event.origin
);
}
};
async performLogin() {
// Alle benötigten Audiences sammeln
const audiences = Array.from(this.microFrontends.values())
.map(mfe => mfe.audience);
// Single Login für alle Audiences
await this.tokenManager.loginWithMultipleAudiences(audiences);
// Alle Micro Frontends benachrichtigen
this.broadcastAuthUpdate();
}
private broadcastAuthUpdate() {
const authChannel = new BroadcastChannel('auth-sync');
authChannel.postMessage({
type: 'AUTH_UPDATE',
payload: {
isAuthenticated: true,
user: this.tokenManager.getUser()
}
});
}
}
Die Token Refresh Strategie#
Token Refresh in Micro Frontends ist tricky. Unser produktionserprobter Ansatz:
// token-refresh-coordinator.ts
class TokenRefreshCoordinator {
private refreshPromises: Map<string, Promise<string>> = new Map();
private refreshTimers: Map<string, NodeJS.Timer> = new Map();
setupAutoRefresh(audience: string, expiresIn: number) {
// Bestehenden Timer löschen
const existingTimer = this.refreshTimers.get(audience);
if (existingTimer) clearTimeout(existingTimer);
// 5 Minuten vor Ablauf refreshen
const refreshIn = (expiresIn - 300) * 1000;
const timer = setTimeout(() => {
this.refreshToken(audience);
}, refreshIn);
this.refreshTimers.set(audience, timer);
}
async refreshToken(audience: string): Promise<string> {
// Gleichzeitigen Refresh für dieselbe Audience verhindern
const existing = this.refreshPromises.get(audience);
if (existing) return existing;
const refreshPromise = this.performRefresh(audience);
this.refreshPromises.set(audience, refreshPromise);
try {
const token = await refreshPromise;
return token;
} finally {
this.refreshPromises.delete(audience);
}
}
private async performRefresh(audience: string): Promise<string> {
try {
// Zuerst Silent Refresh versuchen
const token = await auth0Client.getTokenSilently({
audience: audience,
ignoreCache: true
});
// Dekodieren um Ablaufzeit zu bekommen
const decoded = jwt_decode(token) as any;
const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
// Nächsten Refresh einrichten
this.setupAutoRefresh(audience, expiresIn);
// Storage updaten
this.updateTokenStorage(audience, token);
// Micro Frontends benachrichtigen
this.notifyTokenRefresh(audience, token);
return token;
} catch (error) {
console.error(`Token Refresh fehlgeschlagen für ${audience}:`, error);
// Wenn Refresh fehlschlägt, Re-Authentication versuchen
if (error.error === 'login_required') {
await this.handleLoginRequired();
}
throw error;
}
}
private notifyTokenRefresh(audience: string, token: string) {
// Via BroadcastChannel benachrichtigen
const channel = new BroadcastChannel('auth-sync');
channel.postMessage({
type: 'TOKEN_REFRESHED',
audience: audience,
token: token
});
// Via postMessage an iframes benachrichtigen
const iframes = document.querySelectorAll('iframe');
iframes.forEach(iframe => {
iframe.contentWindow?.postMessage(
{
type: 'TOKEN_REFRESHED',
audience: audience,
token: token
},
'*'
);
});
}
}
Auth0 Rules für Multiple Audiences handhaben#
Auth0 Rules brauchen spezielle Behandlung für Multi-Audience Szenarien:
// auth0-rule.js - Custom Claims für alle Audiences hinzufügen
function addMultiAudienceClaims(user, context, callback) {
// Audience-spezifische Permissions definieren
const audiencePermissions = {
'https://api.myapp.com/billing': ['read:invoices', 'write:payments'],
'https://api.myapp.com/core': ['read:profile', 'read:data'],
'https://api.myapp.com/analytics': ['read:reports', 'read:metrics']
};
// Prüfen welche Audience angefragt wird
const requestedAudience = context.request.query.audience ||
context.request.body.audience;
// Namespace hinzufügen um Kollisionen zu vermeiden
const namespace = 'https://myapp.com/';
// User Metadata zu allen Tokens hinzufügen
context.accessToken[namespace + 'email'] = user.email;
context.accessToken[namespace + 'roles'] = user.app_metadata.roles || [];
// Audience-spezifische Permissions hinzufügen
if (audiencePermissions[requestedAudience]) {
context.accessToken[namespace + 'permissions'] =
audiencePermissions[requestedAudience];
}
// Refresh Token nur für primäre Audience hinzufügen
if (requestedAudience === 'https://api.myapp.com/core') {
context.accessToken[namespace + 'can_refresh'] = true;
}
callback(null, user, context);
}
Silent Authentication: Die Magie hinter der nahtlosen Experience#
Silent Authentication macht den Multi-Audience Ansatz ohne mehrfache Logins möglich:
// silent-auth-handler.ts
class SilentAuthHandler {
private iframe: HTMLIFrameElement | null = null;
private timeoutMs = 60000; // 60 Sekunden
async performSilentAuth(options: SilentAuthOptions): Promise<TokenSet> {
// Hidden iframe für Silent Auth erstellen
this.iframe = this.createAuthIframe();
const authUrl = this.buildAuthUrl(options);
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
this.cleanup();
reject(new Error('Silent authentication timeout'));
}, this.timeoutMs);
// Auf Auth Response hören
const handleMessage = (event: MessageEvent) => {
if (event.origin !== `https://${AUTH0_DOMAIN}`) return;
clearTimeout(timeout);
if (event.data.type === 'authorization_response') {
this.handleAuthResponse(event.data)
.then(resolve)
.catch(reject)
.finally(() => this.cleanup());
}
if (event.data.type === 'authorization_error') {
this.cleanup();
reject(new Error(event.data.error));
}
};
window.addEventListener('message', handleMessage);
// iframe zur Auth URL navigieren
this.iframe.src = authUrl;
});
}
private createAuthIframe(): HTMLIFrameElement {
const iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.style.visibility = 'hidden';
iframe.style.position = 'fixed';
iframe.style.width = '0';
iframe.style.height = '0';
document.body.appendChild(iframe);
return iframe;
}
private buildAuthUrl(options: SilentAuthOptions): string {
const params = new URLSearchParams({
client_id: AUTH0_CLIENT_ID,
response_type: 'token id_token',
redirect_uri: `${window.location.origin}/silent-callback.html`,
audience: options.audience,
scope: options.scope,
state: this.generateState(),
nonce: this.generateNonce(),
prompt: 'none', // Kritisch für Silent Auth
response_mode: 'web_message' // postMessage verwenden
});
return `https://${AUTH0_DOMAIN}/authorize?${params}`;
}
private async handleAuthResponse(response: any): Promise<TokenSet> {
// State und Nonce validieren
if (!this.validateState(response.state)) {
throw new Error('State validation failed');
}
// Tokens aus Response parsen
return {
accessToken: response.access_token,
idToken: response.id_token,
expiresIn: response.expires_in,
tokenType: response.token_type,
audience: response.audience
};
}
private cleanup() {
if (this.iframe && this.iframe.parentNode) {
this.iframe.parentNode.removeChild(this.iframe);
this.iframe = null;
}
}
}
React Native mit WebView Micro Frontends#
Jetzt der wirklich spaßige Teil - das alles in React Native mit WebView-basierten Micro Frontends zum Laufen bringen:
// react-native-auth-bridge.tsx
import React, { useRef, useEffect } from 'react';
import { WebView } from 'react-native-webview';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { authorize, refresh } from 'react-native-app-auth';
interface AuthBridge {
webViewRef: React.RefObject<WebView>;
tokens: Map<string, string>;
}
export function AuthenticatedMicroFrontend({ url, audience }: Props) {
const webViewRef = useRef<WebView>(null);
const [tokens, setTokens] = useState<Map<string, string>>(new Map());
// Auth0 Config für React Native
const auth0Config = {
issuer: `https://${AUTH0_DOMAIN}`,
clientId: AUTH0_CLIENT_ID,
redirectUrl: 'com.myapp://auth/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
additionalParameters: {
audience: audience
},
customHeaders: {
'Auth0-Client': Buffer.from(
JSON.stringify({ name: 'MyApp', version: '1.0.0' })
).toString('base64')
}
};
// Native Authentication
const performNativeAuth = async () => {
try {
// react-native-app-auth für nativen Auth0 Flow verwenden
const result = await authorize(auth0Config);
// Tokens speichern
await AsyncStorage.setItem('auth_tokens', JSON.stringify({
accessToken: result.accessToken,
idToken: result.idToken,
refreshToken: result.refreshToken,
expiresAt: new Date(result.accessTokenExpirationDate).getTime()
}));
// Tokens für andere Audiences holen wenn nötig
await getMultipleAudienceTokens(result.refreshToken);
return result;
} catch (error) {
console.error('Native Auth fehlgeschlagen:', error);
throw error;
}
};
// Bridge zwischen React Native und WebView
const injectedJavaScript = `
(function() {
// Auth0 Client überschreiben um Native Bridge zu verwenden
window.nativeAuth = {
getToken: function(audience) {
return new Promise((resolve, reject) => {
// Unique Request ID generieren
const requestId = Math.random().toString(36).substr(2, 9);
// Response Handler einrichten
window.handleTokenResponse = function(id, token, error) {
if (id !== requestId) return;
if (error) {
reject(new Error(error));
} else {
resolve(token);
}
delete window.handleTokenResponse;
};
// Token von React Native anfordern
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'GET_TOKEN',
audience: audience,
requestId: requestId
}));
});
},
silentAuth: function(options) {
return new Promise((resolve, reject) => {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'SILENT_AUTH',
options: options
}));
window.handleSilentAuthResponse = function(result, error) {
if (error) {
reject(error);
} else {
resolve(result);
}
delete window.handleSilentAuthResponse;
};
});
}
};
// Auth0 Client Initialisierung abfangen
if (window.createAuth0Client) {
const originalCreate = window.createAuth0Client;
window.createAuth0Client = async function(config) {
// Mock Client zurückgeben der Native Bridge verwendet
return {
getTokenSilently: async (options) => {
return window.nativeAuth.getToken(options.audience);
},
loginWithRedirect: async () => {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'LOGIN_REQUIRED'
}));
},
isAuthenticated: async () => {
return window.nativeAuth.isAuthenticated();
}
};
};
}
})();
true; // Erforderlich damit Injection funktioniert
`;
// Nachrichten von WebView behandeln
const handleWebViewMessage = async (event: any) => {
const message = JSON.parse(event.nativeEvent.data);
switch (message.type) {
case 'GET_TOKEN':
await handleTokenRequest(message);
break;
case 'SILENT_AUTH':
await handleSilentAuth(message);
break;
case 'LOGIN_REQUIRED':
await performNativeAuth();
break;
}
};
const handleTokenRequest = async (message: any) => {
try {
// Token für angefragte Audience holen
let token = tokens.get(message.audience);
if (!token || isTokenExpired(token)) {
// Token mit Native Auth refreshen
token = await refreshTokenForAudience(message.audience);
tokens.set(message.audience, token);
}
// Token zurück zur WebView senden
webViewRef.current?.injectJavaScript(`
window.handleTokenResponse(
'${message.requestId}',
'${token}',
null
);
`);
} catch (error) {
// Error zurück zur WebView senden
webViewRef.current?.injectJavaScript(`
window.handleTokenResponse(
'${message.requestId}',
null,
'${error.message}'
);
`);
}
};
const handleSilentAuth = async (message: any) => {
try {
// Prüfen ob wir eine gültige Session haben
const storedTokens = await AsyncStorage.getItem('auth_tokens');
if (storedTokens) {
const tokens = JSON.parse(storedTokens);
if (tokens.expiresAt > Date.now()) {
// Wir haben gültige Tokens, Token für angefragte Audience holen
const audienceToken = await getTokenForAudience(
message.options.audience
);
webViewRef.current?.injectJavaScript(`
window.handleSilentAuthResponse({
accessToken: '${audienceToken}',
expiresIn: 3600
}, null);
`);
return;
}
}
// Refresh versuchen
const refreshed = await refreshAuth();
if (refreshed) {
const audienceToken = await getTokenForAudience(
message.options.audience
);
webViewRef.current?.injectJavaScript(`
window.handleSilentAuthResponse({
accessToken: '${audienceToken}',
expiresIn: 3600
}, null);
`);
} else {
throw new Error('Silent Auth fehlgeschlagen - Login erforderlich');
}
} catch (error) {
webViewRef.current?.injectJavaScript(`
window.handleSilentAuthResponse(null, '${error.message}');
`);
}
};
const refreshAuth = async () => {
try {
const storedTokens = await AsyncStorage.getItem('auth_tokens');
if (!storedTokens) return false;
const { refreshToken } = JSON.parse(storedTokens);
// react-native-app-auth zum Refreshen verwenden
const result = await refresh(auth0Config, {
refreshToken: refreshToken
});
// Gespeicherte Tokens updaten
await AsyncStorage.setItem('auth_tokens', JSON.stringify({
accessToken: result.accessToken,
idToken: result.idToken,
refreshToken: result.refreshToken || refreshToken,
expiresAt: new Date(result.accessTokenExpirationDate).getTime()
}));
return true;
} catch (error) {
console.error('Token Refresh fehlgeschlagen:', error);
return false;
}
};
return (
<WebView
ref={webViewRef}
source={{ uri: url }}
injectedJavaScript={injectedJavaScript}
onMessage={handleWebViewMessage}
sharedCookiesEnabled={true} // Wichtig für Session Sharing
thirdPartyCookiesEnabled={true} // Für Auth0 Cookies
domStorageEnabled={true} // Für localStorage
/>
);
}
Silent Login in React Native: Der komplette Flow#
So funktioniert Silent Login end-to-end in React Native mit Micro Frontends:
// silent-login-flow.ts
class SilentLoginFlow {
private auth0: Auth0Native;
private tokenCache: TokenCache;
private webViewBridge: WebViewBridge;
async performSilentLogin(): Promise<boolean> {
// Schritt 1: Native Token Cache prüfen
const cachedTokens = await this.tokenCache.getTokens();
if (cachedTokens && !this.isExpired(cachedTokens)) {
// Wir haben gültige Tokens, WebView Bridge einrichten
await this.setupWebViewBridge(cachedTokens);
return true;
}
// Schritt 2: Prüfen ob wir Refresh Token haben
const refreshToken = await this.tokenCache.getRefreshToken();
if (refreshToken) {
try {
// Refresh versuchen
const newTokens = await this.auth0.refreshTokens(refreshToken);
await this.tokenCache.storeTokens(newTokens);
await this.setupWebViewBridge(newTokens);
return true;
} catch (error) {
console.log('Refresh fehlgeschlagen, versuche Auth0 Session');
}
}
// Schritt 3: Auth0 Session prüfen (SSO)
try {
const ssoTokens = await this.checkAuth0Session();
if (ssoTokens) {
await this.tokenCache.storeTokens(ssoTokens);
await this.setupWebViewBridge(ssoTokens);
return true;
}
} catch (error) {
console.log('Keine Auth0 Session gefunden');
}
// Schritt 4: Biometrische Authentication Fallback
if (await this.isBiometricAvailable()) {
const bioTokens = await this.attemptBiometricAuth();
if (bioTokens) {
await this.setupWebViewBridge(bioTokens);
return true;
}
}
return false; // Silent Login fehlgeschlagen, expliziter Login nötig
}
private async checkAuth0Session(): Promise<TokenSet | null> {
// Custom Tab / ASWebAuthenticationSession für SSO Check verwenden
const ssoCheckUrl = `https://${AUTH0_DOMAIN}/authorize?` +
`client_id=${CLIENT_ID}&` +
`response_type=token&` +
`redirect_uri=${REDIRECT_URI}&` +
`scope=openid profile email&` +
`prompt=none&` + // Kritisch für Silent Auth
`response_mode=query`;
try {
// Dies öffnet in einer versteckten Web Session
const result = await InAppBrowser.openAuth(ssoCheckUrl, REDIRECT_URI, {
ephemeralWebSession: false, // Shared Session verwenden
preferEphemeralSession: false
});
if (result.type === 'success' && result.url) {
const tokens = this.parseAuthResponse(result.url);
return tokens;
}
} catch (error) {
return null;
}
}
private async setupWebViewBridge(tokens: TokenSet) {
// Tokens in WebView injizieren bevor geladen wird
const script = `
window.__AUTH_TOKENS__ = {
accessToken: '${tokens.accessToken}',
idToken: '${tokens.idToken}',
expiresAt: ${tokens.expiresAt}
};
// Auto-Renewal einrichten
window.__AUTH_BRIDGE__ = {
renewToken: async function(audience) {
return new Promise((resolve) => {
window.ReactNativeWebView.postMessage(JSON.stringify({
type: 'RENEW_TOKEN',
audience: audience
}));
window.__pendingRenewal = resolve;
});
}
};
`;
this.webViewBridge.injectScript(script);
}
}
Gelernte Lektionen: Die Kriegsgeschichten#
1. Das Cookie Problem#
Auth0 verwendet Cookies für Session Management. In React Native WebViews werden Third-Party Cookies oft blockiert. Lösung:
// Cookie Sharing zwischen WebViews aktivieren
const cookieManager = require('@react-native-cookies/cookies');
// Auth0 Cookies über WebViews teilen
await cookieManager.setFromResponse(
`https://${AUTH0_DOMAIN}`,
'auth0_session=...; SameSite=None; Secure'
);
2. Das Token Size Problem#
Multiple Audience Tokens = großer localStorage. Wir haben das 10MB Limit erreicht. Lösung:
// Tokens vor Storage komprimieren
import pako from 'pako';
const compressToken = (token: string): string => {
const compressed = pako.deflate(token, { to: 'string' });
return btoa(compressed);
};
const decompressToken = (compressed: string): string => {
const binary = atob(compressed);
return pako.inflate(binary, { to: 'string' });
};
3. Die Race Condition#
Mehrere Micro Frontends die gleichzeitig Tokens anfordern verursachten Race Conditions. Lösung:
class TokenRequestQueue {
private queue: Map<string, Promise<string>> = new Map();
async getToken(audience: string): Promise<string> {
// Wenn bereits fetching, existierendes Promise zurückgeben
const existing = this.queue.get(audience);
if (existing) return existing;
// Neues Fetch Promise erstellen
const fetchPromise = this.fetchToken(audience);
this.queue.set(audience, fetchPromise);
try {
const token = await fetchPromise;
return token;
} finally {
// Nach Resolution aufräumen
this.queue.delete(audience);
}
}
}
Sicherheitsüberlegungen#
- Token Storage: Niemals Tokens im Klartext speichern. Verschlüsselten Storage verwenden:
import * as Keychain from 'react-native-keychain';
// Tokens sicher speichern
await Keychain.setInternetCredentials(
'auth.myapp.com',
'tokens',
JSON.stringify(tokens),
{
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY
}
);
- WebView Security: Alle Nachrichten validieren:
const validateWebViewMessage = (event: any): boolean => {
// Erlaubte Origins whitelisten
const allowedOrigins = [
'https://billing.myapp.com',
'https://dashboard.myapp.com'
];
if (!allowedOrigins.includes(event.origin)) {
console.error('Ungültige Origin:', event.origin);
return false;
}
// Nachrichtenstruktur validieren
if (!event.data || typeof event.data !== 'object') {
return false;
}
// Nachrichtensignatur validieren (wenn implementiert)
if (!verifyMessageSignature(event.data)) {
return false;
}
return true;
};
Das Fazit#
Multi-Audience Authentication in Micro Frontends ist komplex, aber lösbar. Die Schlüsselerkenntnisse:
- Ein Login, mehrere Tokens: Silent Auth verwenden um Tokens für verschiedene Audiences zu bekommen
- Zentralisiertes Token Management: Die Shell Application orchestrieren lassen
- Message-basierte Kommunikation: postMessage und BroadcastChannel für Cross-Domain verwenden
- Native Bridge für React Native: Auth0 Client in WebViews überschreiben
- Aggressives Caching: Aber mit smarten Refresh-Strategien
Dieses Setup handelt jetzt 50+ Millionen Authentifizierungen pro Monat über unsere Micro Frontends mit 99.9% Erfolgsrate für Silent Authentication.
Denk dran: Die Komplexität liegt in den Edge Cases. Teste mit abgelaufenen Tokens, Netzwerkfehlern und gleichzeitigen Requests. Dein zukünftiges Ich wird dir danken.
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!
Kommentare (0)
An der Unterhaltung teilnehmen
Melde dich an, um deine Gedanken zu teilen und mit der Community zu interagieren
Noch keine Kommentare
Sei der erste, der deine Gedanken zu diesem Beitrag teilt!