Skip to content

Multi-Audience Auth0 Authentication in Micro Frontends: Token Management Patterns and Implementation

Real-world implementation of Auth0 multi-audience authentication across micro frontends, token management strategies, and silent authentication in React Native with WebView-based micro frontends

Abstract

Implementing Auth0 multi-audience authentication across distributed micro frontends presents unique challenges, especially when supporting both web and React Native WebView environments. This case study documents effective patterns for building a unified token management system that serves multiple API audiences from a single login flow.

Key Challenges Addressed:

  • Cross-domain token sharing between micro frontends
  • Multi-audience JWT token management with Auth0
  • Silent authentication in React Native WebViews
  • Token refresh coordination across distributed applications

Solution Overview: A centralized token manager with message-based communication, silent authentication flows, and native-web bridges that eliminated multiple login requirements while maintaining security.

Problem Statement

Micro frontend architectures often reveal critical authentication bottlenecks. This distributed system presented several requirements:

Consider this distributed system architecture:

The Core Problem: Each API requires a different audience in the JWT token, but Auth0's standard flow supports only one audience per authentication. This would force users to authenticate three separate times.

Additional Complexity: The system needed to work seamlessly in React Native WebViews, where traditional web authentication patterns break down due to cookie restrictions and cross-origin limitations.

Failed Approaches

Before reaching our working solution, we explored several approaches that didn't meet our requirements:

Approach 1: Multiple Auth0 Applications

Concept: Create separate Auth0 applications for each micro frontend. Why it failed: Users still needed multiple logins, and managing application configurations became unwieldy. Cross-application session sharing proved unreliable.

Approach 2: Single Audience with Permission Scoping

Concept: Use one audience with fine-grained scopes to control API access. Why it failed: APIs couldn't validate audience-specific permissions properly, and scope management became complex across teams.

Approach 3: Server-Side Token Exchange

Concept: Exchange tokens server-side for different audiences. Why it failed: Added significant latency, required backend changes across all services, and complicated the React Native implementation.

Working Solution: Coordinated Multi-Audience Token Management

An effective approach centers on a shell application that orchestrates authentication for all micro frontends:

1. The Token Manager Architecture

typescript
// token-manager.ts - Core token management implementationimport { Auth0Client } from '@auth0/auth0-spa-js'; // ^2.1.3import jwt_decode from 'jwt-decode'; // ^4.0.0
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', // Critical for micro frontends      useRefreshTokens: true,      authorizeTimeoutInSeconds: 60    });  }
  async loginWithMultipleAudiences(audiences: string[]): Promise<void> {    // Step 1: Login with primary audience (includes all scopes)    const primaryAudience = audiences[0];    const allScopes = this.getAllRequiredScopes();
    const result = await this.auth0Client.loginWithRedirect({      audience: primaryAudience,      scope: allScopes,      redirect_uri: window.location.origin    });
    // After redirect callback    const tokens = await this.auth0Client.handleRedirectCallback();    this.primaryRefreshToken = tokens.refreshToken;
    // Store primary token    this.storeToken(primaryAudience, tokens);
    // Step 2: Get tokens for other audiences silently    for (const audience of audiences.slice(1)) {      await this.getTokenForAudience(audience);    }  }
  async getTokenForAudience(audience: string): Promise<string> {    // Check cache first    const cached = this.tokens.get(audience);    if (cached && cached.expiresAt > Date.now()) {      return cached.accessToken;    }
    try {      // Try silent authentication first      const token = await this.auth0Client.getTokenSilently({        audience: audience,        scope: this.getScopeForAudience(audience),        cacheMode: 'off' // Force fresh token      });
      this.storeToken(audience, {        accessToken: token,        expiresAt: Date.now() + 3600000, // 1 hour        audience: audience,        scope: this.getScopeForAudience(audience)      });
      return token;    } catch (error) {      // If silent auth fails, use refresh token      if (this.primaryRefreshToken) {        return this.refreshTokenForAudience(audience);      }      throw error;    }  }
  private async refreshTokenForAudience(audience: string): Promise<string> {    // Auth0 token endpoint with 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 for Micro Frontends

The biggest challenge: sharing tokens across different subdomains. Here's an effective solution:

typescript
// shared-auth-context.tsx - Used by all micro frontendsimport { createContext, useContext, useEffect, useState } from 'react';import { Auth0Client } from '@auth0/auth0-spa-js';
interface SharedAuthState {  isAuthenticated: boolean;  tokens: Map<string, string>;  user: any;}
const SharedAuthContext = createContext<SharedAuthState | null>(null);
// Broadcast channel for cross-tab/cross-iframe communicationconst authChannel = new BroadcastChannel('auth-sync');
export function SharedAuthProvider({ children, audience }: Props) {  const [authState, setAuthState] = useState<SharedAuthState>();  const [tokenManager] = useState(() => new MultiAudienceTokenManager());
  useEffect(() => {    // Listen for auth updates from other micro frontends    authChannel.onmessage = (event) => {      if (event.data.type === 'AUTH_UPDATE') {        setAuthState(event.data.payload);      }    };
    // Check if we're authenticated via shared storage    checkSharedAuthentication();  }, []);
  const checkSharedAuthentication = async () => {    // Try multiple storage strategies
    // Strategy 1: Shared localStorage via iframe postMessage    const sharedToken = await getTokenFromShell();
    // Strategy 2: Server-side session check    if (!sharedToken) {      const session = await checkServerSession();      if (session) {        await silentAuthentication();      }    }
    // Strategy 3: Auth0 session check    if (!sharedToken) {      const auth0Session = await checkAuth0Session();      if (auth0Session) {        await getTokenSilently();      }    }  };
  const getTokenFromShell = (): Promise<string | null> => {    return new Promise((resolve) => {      // Post message to shell application      window.parent.postMessage(        { type: 'GET_TOKEN', audience },        'https://auth.myapp.com'      );
      // Listen for response      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 after 1 second      setTimeout(() => {        window.removeEventListener('message', handler);        resolve(null);      }, 1000);    });  };
  return (    <SharedAuthContext.Provider value={authState}>      {children}    </SharedAuthContext.Provider>  );}

3. The Shell Application - Orchestrating Authentication

typescript
// shell-application.tsx - The authentication orchestratorclass ShellAuthOrchestrator {  private microFrontends: Map<string, MicroFrontendConfig> = new Map();  private tokenManager: MultiAudienceTokenManager;  private sessionManager: SessionManager;
  async initialize() {    // Register all micro frontends and their required audiences    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']      }    ]);
    // Setup message handler for micro frontend token requests    window.addEventListener('message', this.handleTokenRequest);
    // Check authentication status    await this.checkAuthentication();  }
  private handleTokenRequest = async (event: MessageEvent) => {    // Validate origin    const mfe = this.getMicroFrontendByOrigin(event.origin);    if (!mfe) return;
    if (event.data.type === 'GET_TOKEN') {      const token = await this.tokenManager.getTokenForAudience(        event.data.audience      );
      // Send token back to requesting micro frontend      event.source?.postMessage(        {          type: 'TOKEN_RESPONSE',          token: token,          audience: event.data.audience        },        event.origin      );    }  };
  async performLogin() {    // Collect all required audiences    const audiences = Array.from(this.microFrontends.values())      .map(mfe => mfe.audience);
    // Single login for all audiences    await this.tokenManager.loginWithMultipleAudiences(audiences);
    // Notify all micro frontends    this.broadcastAuthUpdate();  }
  private broadcastAuthUpdate() {    const authChannel = new BroadcastChannel('auth-sync');    authChannel.postMessage({      type: 'AUTH_UPDATE',      payload: {        isAuthenticated: true,        user: this.tokenManager.getUser()      }    });  }}

The Token Refresh Strategy

Token refresh in micro frontends is tricky. Here's an effective approach for production:

typescript
// token-refresh-coordinator.tsclass TokenRefreshCoordinator {  private refreshPromises: Map<string, Promise<string>> = new Map();  private refreshTimers: Map<string, NodeJS.Timer> = new Map();
  setupAutoRefresh(audience: string, expiresIn: number) {    // Clear existing timer    const existingTimer = this.refreshTimers.get(audience);    if (existingTimer) clearTimeout(existingTimer);
    // Refresh 5 minutes before expiry    const refreshIn = (expiresIn - 300) * 1000;
    const timer = setTimeout(() => {      this.refreshToken(audience);    }, refreshIn);
    this.refreshTimers.set(audience, timer);  }
  async refreshToken(audience: string): Promise<string> {    // Prevent concurrent refresh for same audience    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 {      // Try silent refresh first      const token = await auth0Client.getTokenSilently({        audience: audience,        ignoreCache: true      });
      // Decode to get expiry      const decoded = jwt_decode(token) as any;      const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);
      // Setup next refresh      this.setupAutoRefresh(audience, expiresIn);
      // Update storage      this.updateTokenStorage(audience, token);
      // Notify micro frontends      this.notifyTokenRefresh(audience, token);
      return token;    } catch (error) {      console.error(`Token refresh failed for \${audience}:`, error);
      // If refresh fails, try re-authentication      if (error.error === 'login_required') {        await this.handleLoginRequired();      }
      throw error;    }  }
  private notifyTokenRefresh(audience: string, token: string) {    // Notify via BroadcastChannel    const channel = new BroadcastChannel('auth-sync');    channel.postMessage({      type: 'TOKEN_REFRESHED',      audience: audience,      token: token    });
    // Notify via postMessage to iframes    const iframes = document.querySelectorAll('iframe');    iframes.forEach(iframe => {      iframe.contentWindow?.postMessage(        {          type: 'TOKEN_REFRESHED',          audience: audience,          token: token        },        '*'      );    });  }}

Auth0 Actions for Multi-Audience Support

Important: Auth0 Rules were deprecated as of November 2024. Use Actions instead for multi-audience scenarios:

javascript
// auth0-action.js - Add custom claims for all audiences using Actionsexports.onExecutePostLogin = async (event, api) => {  const { user, request } = event;
  // Define audience-specific permissions  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']  };
  // Check which audience is being requested  const requestedAudience = request.query?.audience || request.body?.audience;
  // Add namespace to avoid collision  const namespace = 'https://myapp.com/';
  // Add user metadata to all tokens  api.accessToken.setCustomClaim(namespace + 'email', user.email);  api.accessToken.setCustomClaim(namespace + 'roles', user.app_metadata?.roles || []);
  // Add audience-specific permissions  if (audiencePermissions[requestedAudience]) {    api.accessToken.setCustomClaim(namespace + 'permissions', audiencePermissions[requestedAudience]);  }
  // Add refresh token indicator for primary audience only  if (requestedAudience === 'https://api.myapp.com/core') {    api.accessToken.setCustomClaim(namespace + 'can_refresh', true);  }};

Silent Authentication: The Magic Behind Seamless Experience

Silent authentication is what makes the multi-audience approach work without multiple logins:

typescript
// silent-auth-handler.tsclass SilentAuthHandler {  private iframe: HTMLIFrameElement | null = null;  private timeoutMs = 60000; // 60 seconds
  async performSilentAuth(options: SilentAuthOptions): Promise<TokenSet> {    // Create hidden iframe for silent auth    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);
      // Listen for auth response      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);
      // Navigate iframe to auth URL      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', // Critical for silent auth      response_mode: 'web_message' // Use postMessage    });
    return `https://${AUTH0_DOMAIN}/authorize?${params}`;  }
  private async handleAuthResponse(response: any): Promise<TokenSet> {    // Validate state and nonce    if (!this.validateState(response.state)) {      throw new Error('State validation failed');    }
    // Parse tokens from response    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 with WebView Micro Frontends

Now the really fun part - making all this work in React Native with WebView-based micro frontends:

typescript
// react-native-auth-bridge.tsximport React, { useRef, useEffect } from 'react';import { WebView } from 'react-native-webview'; // ^13.8.6import AsyncStorage from '@react-native-async-storage/async-storage'; // ^1.23.1import { authorize, refresh } from 'react-native-app-auth'; // ^7.1.0
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());
  // react-native-app-auth config - chosen for its Auth0 compatibility  // and native authentication session support on iOS/Android  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 {      // Use react-native-app-auth for native Auth0 flow      const result = await authorize(auth0Config);
      // Store tokens      await AsyncStorage.setItem('auth_tokens', JSON.stringify({        accessToken: result.accessToken,        idToken: result.idToken,        refreshToken: result.refreshToken,        expiresAt: new Date(result.accessTokenExpirationDate).getTime()      }));
      // Get tokens for other audiences if needed      await getMultipleAudienceTokens(result.refreshToken);
      return result;    } catch (error) {      console.error('Native auth failed:', error);      throw error;    }  };
  // Bridge between React Native and WebView  const injectedJavaScript = `    (function() {      // Override Auth0 client to use native bridge      window.nativeAuth = {        getToken: function(audience) {          return new Promise((resolve, reject) => {            // Generate unique request ID            const requestId = Math.random().toString(36).substr(2, 9);
            // Setup response handler            window.handleTokenResponse = function(id, token, error) {              if (id !== requestId) return;
              if (error) {                reject(new Error(error));              } else {                resolve(token);              }
              delete window.handleTokenResponse;            };
            // Request token from React Native            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;            };          });        }      };
      // Intercept Auth0 client initialization      if (window.createAuth0Client) {        const originalCreate = window.createAuth0Client;        window.createAuth0Client = async function(config) {          // Return mock client that uses native bridge          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; // Required for injection to work  `;
  // Handle messages from WebView  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 {      // Get token for requested audience      let token = tokens.get(message.audience);
      if (!token || isTokenExpired(token)) {        // Refresh token using native auth        token = await refreshTokenForAudience(message.audience);        tokens.set(message.audience, token);      }
      // Send token back to WebView      webViewRef.current?.injectJavaScript(`        window.handleTokenResponse(          '${message.requestId}',          '${token}',          null        );      `);    } catch (error) {      // Send error back to WebView      webViewRef.current?.injectJavaScript(`        window.handleTokenResponse(          '${message.requestId}',          null,          '${error.message}'        );      `);    }  };
  const handleSilentAuth = async (message: any) => {    try {      // Check if we have valid session      const storedTokens = await AsyncStorage.getItem('auth_tokens');
      if (storedTokens) {        const tokens = JSON.parse(storedTokens);
        if (tokens.expiresAt > Date.now()) {          // We have valid tokens, get token for requested audience          const audienceToken = await getTokenForAudience(            message.options.audience          );
          webViewRef.current?.injectJavaScript(`            window.handleSilentAuthResponse({              accessToken: '${audienceToken}',              expiresIn: 3600            }, null);          `);          return;        }      }
      // Try to refresh      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 failed - login required');      }    } 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);
      // Use react-native-app-auth to refresh      const result = await refresh(auth0Config, {        refreshToken: refreshToken      });
      // Update stored tokens      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 failed:', error);      return false;    }  };
  return (    <WebView      ref={webViewRef}      source={{ uri: url }}      injectedJavaScript={injectedJavaScript}      onMessage={handleWebViewMessage}      sharedCookiesEnabled={true} // Important for session sharing      thirdPartyCookiesEnabled={true} // For Auth0 cookies      domStorageEnabled={true} // For localStorage    />  );}

Silent Login in React Native: The Complete Flow

Here's how silent login works end-to-end in React Native with micro frontends:

typescript
// silent-login-flow.tsclass SilentLoginFlow {  private auth0: Auth0Native;  private tokenCache: TokenCache;  private webViewBridge: WebViewBridge;
  async performSilentLogin(): Promise<boolean> {    // Step 1: Check native token cache    const cachedTokens = await this.tokenCache.getTokens();
    if (cachedTokens && !this.isExpired(cachedTokens)) {      // We have valid tokens, setup WebView bridge      await this.setupWebViewBridge(cachedTokens);      return true;    }
    // Step 2: Check if we have refresh token    const refreshToken = await this.tokenCache.getRefreshToken();
    if (refreshToken) {      try {        // Attempt refresh        const newTokens = await this.auth0.refreshTokens(refreshToken);        await this.tokenCache.storeTokens(newTokens);        await this.setupWebViewBridge(newTokens);        return true;      } catch (error) {        console.log('Refresh failed, trying Auth0 session');      }    }
    // Step 3: Check Auth0 session (SSO)    try {      const ssoTokens = await this.checkAuth0Session();      if (ssoTokens) {        await this.tokenCache.storeTokens(ssoTokens);        await this.setupWebViewBridge(ssoTokens);        return true;      }    } catch (error) {      console.log('No Auth0 session found');    }
    // Step 4: Biometric authentication fallback    if (await this.isBiometricAvailable()) {      const bioTokens = await this.attemptBiometricAuth();      if (bioTokens) {        await this.setupWebViewBridge(bioTokens);        return true;      }    }
    return false; // Silent login failed, need explicit login  }
  private async checkAuth0Session(): Promise<TokenSet | null> {    // Use custom tab / ASWebAuthenticationSession for SSO check    const ssoCheckUrl = `https://${AUTH0_DOMAIN}/authorize?` +      `client_id=${CLIENT_ID}&` +      `response_type=token&` +      `redirect_uri=${REDIRECT_URI}&` +      `scope=openid profile email&` +      `prompt=none&` + // Critical for silent auth      `response_mode=query`;
    try {      // This opens in a hidden web session      const result = await InAppBrowser.openAuth(ssoCheckUrl, REDIRECT_URI, {        ephemeralWebSession: false, // Use shared session        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) {    // Inject tokens into WebView before loading    const script = `      window.__AUTH_TOKENS__ = {        accessToken: '${tokens.accessToken}',        idToken: '${tokens.idToken}',        expiresAt: ${tokens.expiresAt}      };
      // Setup auto-renewal      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);  }}

Multi-Resource Refresh Tokens (MRRT)

Auth0 introduced Multi-Resource Refresh Tokens as a new feature for multi-audience scenarios. This allows a single refresh token to obtain access tokens for multiple audiences:

typescript
// Using MRRT for efficient multi-audience token refreshclass MRRTTokenManager {  async refreshMultipleAudiences(refreshToken: string, audiences: string[]): Promise<Map<string, TokenSet>> {    const tokens = new Map<string, TokenSet>();
    // MRRT allows one refresh token to get tokens for multiple audiences    for (const audience of audiences) {      try {        const response = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {          method: 'POST',          headers: { 'Content-Type': 'application/json' },          body: JSON.stringify({            grant_type: 'refresh_token',            client_id: CLIENT_ID,            refresh_token: refreshToken,            audience: audience          })        });
        const data = await response.json();        tokens.set(audience, {          accessToken: data.access_token,          expiresAt: Date.now() + (data.expires_in * 1000),          audience: audience        });      } catch (error) {        console.error(`MRRT refresh failed for ${audience}:`, error);      }    }
    return tokens;  }}

Security Considerations

Critical Warning: Always validate JWT tokens on the backend. Never trust client-side token validation alone.

typescript
// Backend JWT validation with proper error handlingimport jwt from 'jsonwebtoken';import jwksClient from 'jwks-rsa';
const client = jwksClient({  jwksUri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`,  cache: true,  cacheMaxAge: 600000 // 10 minutes});
function getKey(header: any, callback: Function) {  client.getSigningKey(header.kid, (err: Error | null, key: any) => {    if (err) {      callback(err);      return;    }    const signingKey = key.publicKey || key.rsaPublicKey;    callback(null, signingKey);  });}
// Validate token with comprehensive error handlingconst verifyToken = (token: string, audience: string): Promise<any> => {  return new Promise((resolve, reject) => {    jwt.verify(token, getKey, {      audience: audience,      issuer: `https://${AUTH0_DOMAIN}/`,      algorithms: ['RS256']    }, (err: Error | null, decoded: any) => {      if (err) {        console.error('JWT verification failed:', err.message);        reject(err);      } else {        resolve(decoded);      }    });  });};

Essential Security Measures:

  1. Token Storage: Use secure, encrypted storage for tokens
  2. Origin Validation: Always validate message origins in postMessage handlers
  3. HTTPS Only: Never transmit tokens over unencrypted connections
  4. Token Rotation: Implement proper refresh token rotation
  5. Audience Validation: Verify audience claims match expected values

Implementation Lessons Learned

Auth0 uses cookies for session management. In React Native WebViews, third-party cookies are often blocked. Solution:

typescript
// Enable cookie sharing between WebViewsconst cookieManager = require('@react-native-cookies/cookies');
// Share Auth0 cookies across WebViewsawait cookieManager.setFromResponse(  `https://${AUTH0_DOMAIN}`,  'auth0_session=...; SameSite=None; Secure');

2. The Token Size Problem

Multiple audience tokens = large localStorage. We hit the 10MB limit. Solution:

typescript
// Compress tokens before storageimport 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. The Race Condition

Multiple micro frontends requesting tokens simultaneously caused race conditions. Solution:

typescript
class TokenRequestQueue {  private queue: Map<string, Promise<string>> = new Map();
  async getToken(audience: string): Promise<string> {    // If already fetching, return existing promise    const existing = this.queue.get(audience);    if (existing) return existing;
    // Create new fetch promise    const fetchPromise = this.fetchToken(audience);    this.queue.set(audience, fetchPromise);
    try {      const token = await fetchPromise;      return token;    } finally {      // Clean up after resolution      this.queue.delete(audience);    }  }}

React Native Security Implementation

  1. Secure Token Storage: Never store tokens in plain text. Use encrypted storage:
typescript
import * as Keychain from 'react-native-keychain';
// Store tokens with biometric protectionconst storeTokensSecurely = async (tokens: TokenSet) => {  try {    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,        service: 'myapp-auth' // Unique service identifier      }    );  } catch (error) {    console.error('Secure storage failed:', error);    throw new Error('Failed to store authentication tokens securely');  }};
  1. WebView Security with Message Validation:
typescript
const validateWebViewMessage = (event: any): boolean => {  // Whitelist allowed origins  const allowedOrigins = [    'https://billing.myapp.com',    'https://dashboard.myapp.com',    'https://analytics.myapp.com'  ];
  if (!allowedOrigins.includes(event.origin)) {    console.error('Invalid origin:', event.origin);    return false;  }
  // Validate message structure  if (!event.data || typeof event.data !== 'object') {    console.error('Invalid message structure');    return false;  }
  // Validate required fields  const requiredFields = ['type', 'requestId'];  for (const field of requiredFields) {    if (!event.data[field]) {      console.error(`Missing required field: ${field}`);      return false;    }  }
  return true;};

Key Implementation Patterns

Multi-audience authentication in micro frontends requires careful architectural planning:

  1. Centralized Token Management: Design token orchestration as a dedicated service from the start
  2. Edge Case Testing: Expired tokens, network failures, and race conditions reveal implementation complexity
  3. Security-First Design: Implement comprehensive JWT validation and encrypted token storage
  4. Progressive Enhancement: Start with web implementation, then extend to React Native WebViews
  5. Clear Message Contracts: Define explicit communication protocols between components
  6. MRRT Integration: Leverage Auth0's Multi-Resource Refresh Tokens for efficient token management

These patterns work effectively in production environments, achieving reliable authentication flows across distributed micro frontends with consistent silent authentication success rates.

Complex authentication scenarios become manageable through systematic architecture and security-focused implementation. The token flow design should be the foundation before tackling specific technical details.

References

Related Posts