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#

TypeScript
// 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:

TypeScript
// 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#

TypeScript
// 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:

TypeScript
// 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:

JavaScript
// 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:

TypeScript
// 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:

TypeScript
// 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:

TypeScript
// 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:

TypeScript
// 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:

TypeScript
// 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:

TypeScript
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#

  1. Token Storage: Niemals Tokens im Klartext speichern. Verschlüsselten Storage verwenden:
TypeScript
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
  }
);
  1. WebView Security: Alle Nachrichten validieren:
TypeScript
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:

  1. Ein Login, mehrere Tokens: Silent Auth verwenden um Tokens für verschiedene Audiences zu bekommen
  2. Zentralisiertes Token Management: Die Shell Application orchestrieren lassen
  3. Message-basierte Kommunikation: postMessage und BroadcastChannel für Cross-Domain verwenden
  4. Native Bridge für React Native: Auth0 Client in WebViews überschreiben
  5. 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.

Loading...

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!

Related Posts