Micro Frontend'lerde Multi-Audience Auth0: Çözdüğümüz Token Yönetimi Kabusları

Micro frontend'lerde Auth0 multi-audience authentication gerçek dünya implementasyonu, token yönetim stratejileri ve React Native'de WebView tabanlı micro frontend'lerle silent authentication

Üç ay önce "basit" micro frontend migrasyonumuz bir authentication kabusuna dönüştü. Beş farklı takım, sekiz micro frontend, üç API ve hepsini Auth0 üzerinden kullanıcılar birden fazla kez login olmadan authenticate etmemiz gerekiyordu. İşin püf noktası? React Native uygulamamızda bu micro frontend'leri WebView'larda embed ettiğimizde de çalışması gerekiyordu. Bu karmaşayı nasıl çözdüğümüzü ve gerçekten çalışan bir token yönetim sistemi kurduğumuzu anlatacağım.

Problem: Birden Fazla Audience, Tek Login#

Bu mimariyi hayal edin:

Loading diagram...

Her API, JWT token'da farklı bir audience istiyor. Auth0'nun default flow'u? Authentication başına bir audience. Kullanıcılarımız üç kez login olmalıydı. Olmaz.

Çözüm: Multi-Audience Token Yönetimi#

İki hafta Auth0 dokümantasyonuna dalıp birkaç sabah 2'de debug session'larından sonra, gerçekten işe yarayan:

1. Token Manager Mimarisi#

TypeScript
// token-manager.ts - Auth sistemimizin beyni
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', // Micro frontend'ler için kritik
      useRefreshTokens: true,
      authorizeTimeoutInSeconds: 60
    });
  }

  async loginWithMultipleAudiences(audiences: string[]): Promise<void> {
    // Adım 1: Primary audience ile login (tüm scope'ları içerir)
    const primaryAudience = audiences[0];
    const allScopes = this.getAllRequiredScopes();

    const result = await this.auth0Client.loginWithRedirect({
      audience: primaryAudience,
      scope: allScopes,
      redirect_uri: window.location.origin
    });

    // Redirect callback'ten sonra
    const tokens = await this.auth0Client.handleRedirectCallback();
    this.primaryRefreshToken = tokens.refreshToken;

    // Primary token'ı sakla
    this.storeToken(primaryAudience, tokens);

    // Adım 2: Diğer audience'lar için sessizce token al
    for (const audience of audiences.slice(1)) {
      await this.getTokenForAudience(audience);
    }
  }

  async getTokenForAudience(audience: string): Promise<string> {
    // Önce cache'i kontrol et
    const cached = this.tokens.get(audience);
    if (cached && cached.expiresAt > Date.now()) {
      return cached.accessToken;
    }

    try {
      // Önce silent authentication dene
      const token = await this.auth0Client.getTokenSilently({
        audience: audience,
        scope: this.getScopeForAudience(audience),
        cacheMode: 'off' // Fresh token zorla
      });

      this.storeToken(audience, {
        accessToken: token,
        expiresAt: Date.now() + 3600000, // 1 saat
        audience: audience,
        scope: this.getScopeForAudience(audience)
      });

      return token;
    } catch (error) {
      // Silent auth başarısız olursa, refresh token kullan
      if (this.primaryRefreshToken) {
        return this.refreshTokenForAudience(audience);
      }
      throw error;
    }
  }

  private async refreshTokenForAudience(audience: string): Promise<string> {
    // Auth0 token endpoint'i ile 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. Micro Frontend'ler için Cross-Domain Token Paylaşımı#

En büyük zorluk: Token'ları farklı subdomain'ler arasında paylaşmak. Test edilmiş çözümümüz:

TypeScript
// shared-auth-context.tsx - Tüm micro frontend'ler tarafından kullanılır
import { createContext, useContext, useEffect, useState } from 'react';

interface SharedAuthState {
  isAuthenticated: boolean;
  tokens: Map<string, string>;
  user: any;
}

const SharedAuthContext = createContext<SharedAuthState | null>(null);

// Cross-tab/cross-iframe iletişim için broadcast channel
const authChannel = new BroadcastChannel('auth-sync');

export function SharedAuthProvider({ children, audience }: Props) {
  const [authState, setAuthState] = useState<SharedAuthState>();
  const [tokenManager] = useState(() => new MultiAudienceTokenManager());

  useEffect(() => {
    // Diğer micro frontend'lerden auth güncellemelerini dinle
    authChannel.onmessage = (event) => {
      if (event.data.type === 'AUTH_UPDATE') {
        setAuthState(event.data.payload);
      }
    };

    // Shared storage üzerinden authenticate olup olmadığımızı kontrol et
    checkSharedAuthentication();
  }, []);

  const checkSharedAuthentication = async () => {
    // Birden fazla storage stratejisi dene

    // Strateji 1: iframe postMessage ile shared localStorage
    const sharedToken = await getTokenFromShell();

    // Strateji 2: Server-side session kontrolü
    if (!sharedToken) {
      const session = await checkServerSession();
      if (session) {
        await silentAuthentication();
      }
    }

    // Strateji 3: Auth0 session kontrolü
    if (!sharedToken) {
      const auth0Session = await checkAuth0Session();
      if (auth0Session) {
        await getTokenSilently();
      }
    }
  };

  const getTokenFromShell = (): Promise<string | null> => {
    return new Promise((resolve) => {
      // Shell application'a mesaj gönder
      window.parent.postMessage(
        { type: 'GET_TOKEN', audience },
        'https://auth.myapp.com'
      );

      // Cevabı dinle
      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);

      // 1 saniye sonra timeout
      setTimeout(() => {
        window.removeEventListener('message', handler);
        resolve(null);
      }, 1000);
    });
  };

  return (
    <SharedAuthContext.Provider value={authState}>
      {children}
    </SharedAuthContext.Provider>
  );
}

3. Shell Application - Authentication Orkestratörü#

TypeScript
// shell-application.tsx - Authentication orkestratörü
class ShellAuthOrchestrator {
  private microFrontends: Map<string, MicroFrontendConfig> = new Map();
  private tokenManager: MultiAudienceTokenManager;
  private sessionManager: SessionManager;

  async initialize() {
    // Tüm micro frontend'leri ve gereken audience'larını kaydet
    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']
      }
    ]);

    // Micro frontend token request'leri için message handler kur
    window.addEventListener('message', this.handleTokenRequest);

    // Authentication durumunu kontrol et
    await this.checkAuthentication();
  }

  private handleTokenRequest = async (event: MessageEvent) => {
    // Origin'i validate et
    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'ı isteyen micro frontend'e geri gönder
      event.source?.postMessage(
        {
          type: 'TOKEN_RESPONSE',
          token: token,
          audience: event.data.audience
        },
        event.origin
      );
    }
  };

  async performLogin() {
    // Tüm gereken audience'ları topla
    const audiences = Array.from(this.microFrontends.values())
      .map(mfe => mfe.audience);

    // Tüm audience'lar için tek login
    await this.tokenManager.loginWithMultipleAudiences(audiences);

    // Tüm micro frontend'lere bildir
    this.broadcastAuthUpdate();
  }

  private broadcastAuthUpdate() {
    const authChannel = new BroadcastChannel('auth-sync');
    authChannel.postMessage({
      type: 'AUTH_UPDATE',
      payload: {
        isAuthenticated: true,
        user: this.tokenManager.getUser()
      }
    });
  }
}

Token Refresh Stratejisi#

Micro frontend'lerde token refresh zor. Production'da test edilmiş yaklaşımımız:

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) {
    // Mevcut timer'ı temizle
    const existingTimer = this.refreshTimers.get(audience);
    if (existingTimer) clearTimeout(existingTimer);

    // Expiry'den 5 dakika önce refresh et
    const refreshIn = (expiresIn - 300) * 1000;

    const timer = setTimeout(() => {
      this.refreshToken(audience);
    }, refreshIn);

    this.refreshTimers.set(audience, timer);
  }

  async refreshToken(audience: string): Promise<string> {
    // Aynı audience için concurrent refresh'i önle
    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 {
      // Önce silent refresh dene
      const token = await auth0Client.getTokenSilently({
        audience: audience,
        ignoreCache: true
      });

      // Expiry almak için decode et
      const decoded = jwt_decode(token) as any;
      const expiresIn = decoded.exp - Math.floor(Date.now() / 1000);

      // Sonraki refresh'i kur
      this.setupAutoRefresh(audience, expiresIn);

      // Storage'ı güncelle
      this.updateTokenStorage(audience, token);

      // Micro frontend'lere bildir
      this.notifyTokenRefresh(audience, token);

      return token;
    } catch (error) {
      console.error(`Token refresh failed for ${audience}:`, error);

      // Refresh başarısız olursa, re-authentication dene
      if (error.error === 'login_required') {
        await this.handleLoginRequired();
      }

      throw error;
    }
  }

  private notifyTokenRefresh(audience: string, token: string) {
    // BroadcastChannel ile bildir
    const channel = new BroadcastChannel('auth-sync');
    channel.postMessage({
      type: 'TOKEN_REFRESHED',
      audience: audience,
      token: token
    });

    // iframe'lere postMessage ile bildir
    const iframes = document.querySelectorAll('iframe');
    iframes.forEach(iframe => {
      iframe.contentWindow?.postMessage(
        {
          type: 'TOKEN_REFRESHED',
          audience: audience,
          token: token
        },
        '*'
      );
    });
  }
}

Multiple Audience'lar için Auth0 Rules#

Auth0 Rules multi-audience senaryoları için özel handling gerektirir:

JavaScript
// auth0-rule.js - Tüm audience'lar için custom claim'ler ekle
function addMultiAudienceClaims(user, context, callback) {
  // Audience-specific permission'ları tanımla
  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']
  };

  // Hangi audience istendiğini kontrol et
  const requestedAudience = context.request.query.audience ||
                           context.request.body.audience;

  // Collision önlemek için namespace ekle
  const namespace = 'https://myapp.com/';

  // Tüm token'lara user metadata ekle
  context.accessToken[namespace + 'email'] = user.email;
  context.accessToken[namespace + 'roles'] = user.app_metadata.roles || [];

  // Audience-specific permission'ları ekle
  if (audiencePermissions[requestedAudience]) {
    context.accessToken[namespace + 'permissions'] =
      audiencePermissions[requestedAudience];
  }

  // Sadece primary audience için refresh token ekle
  if (requestedAudience === 'https://api.myapp.com/core') {
    context.accessToken[namespace + 'can_refresh'] = true;
  }

  callback(null, user, context);
}

Silent Authentication: Sorunsuz Deneyimin Arkasındaki Sihir#

Silent authentication, multi-audience yaklaşımının birden fazla login olmadan çalışmasını sağlayan şey:

TypeScript
// silent-auth-handler.ts
class SilentAuthHandler {
  private iframe: HTMLIFrameElement | null = null;
  private timeoutMs = 60000; // 60 saniye

  async performSilentAuth(options: SilentAuthOptions): Promise<TokenSet> {
    // Silent auth için hidden iframe oluştur
    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);

      // Auth response'u dinle
      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'i auth URL'e navigate et
      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', // Silent auth için kritik
      response_mode: 'web_message' // postMessage kullan
    });

    return `https://${AUTH0_DOMAIN}/authorize?${params}`;
  }

  private async handleAuthResponse(response: any): Promise<TokenSet> {
    // State ve nonce'u validate et
    if (!this.validateState(response.state)) {
      throw new Error('State validation failed');
    }

    // Response'tan token'ları parse et
    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'de WebView Micro Frontend'ler#

Şimdi gerçekten eğlenceli kısım - tüm bunları React Native'de WebView tabanlı micro frontend'lerle çalıştırmak:

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());

  // React Native için Auth0 config
  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 {
      // Native Auth0 flow için react-native-app-auth kullan
      const result = await authorize(auth0Config);

      // Token'ları sakla
      await AsyncStorage.setItem('auth_tokens', JSON.stringify({
        accessToken: result.accessToken,
        idToken: result.idToken,
        refreshToken: result.refreshToken,
        expiresAt: new Date(result.accessTokenExpirationDate).getTime()
      }));

      // Gerekirse diğer audience'lar için token'ları al
      await getMultipleAudienceTokens(result.refreshToken);

      return result;
    } catch (error) {
      console.error('Native auth başarısız:', error);
      throw error;
    }
  };

  // React Native ve WebView arasında köprü
  const injectedJavaScript = `
    (function() {
      // Native bridge kullanmak için Auth0 client'ı override et
      window.nativeAuth = {
        getToken: function(audience) {
          return new Promise((resolve, reject) => {
            // Unique request ID oluştur
            const requestId = Math.random().toString(36).substr(2, 9);

            // Response handler kur
            window.handleTokenResponse = function(id, token, error) {
              if (id !== requestId) return;

              if (error) {
                reject(new Error(error));
              } else {
                resolve(token);
              }

              delete window.handleTokenResponse;
            };

            // React Native'den token iste
            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 initialization'ı intercept et
      if (window.createAuth0Client) {
        const originalCreate = window.createAuth0Client;
        window.createAuth0Client = async function(config) {
          // Native bridge kullanan mock client döndür
          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; // Injection'ın çalışması için gerekli
  `;

  // WebView'dan mesajları handle et
  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 {
      // İstenen audience için token al
      let token = tokens.get(message.audience);

      if (!token || isTokenExpired(token)) {
        // Native auth kullanarak token'ı refresh et
        token = await refreshTokenForAudience(message.audience);
        tokens.set(message.audience, token);
      }

      // Token'ı WebView'a geri gönder
      webViewRef.current?.injectJavaScript(`
        window.handleTokenResponse(
          '${message.requestId}',
          '${token}',
          null
        );
      `);
    } catch (error) {
      // Error'u WebView'a geri gönder
      webViewRef.current?.injectJavaScript(`
        window.handleTokenResponse(
          '${message.requestId}',
          null,
          '${error.message}'
        );
      `);
    }
  };

  const handleSilentAuth = async (message: any) => {
    try {
      // Valid session var mı kontrol et
      const storedTokens = await AsyncStorage.getItem('auth_tokens');

      if (storedTokens) {
        const tokens = JSON.parse(storedTokens);

        if (tokens.expiresAt > Date.now()) {
          // Valid token'larımız var, istenen audience için token al
          const audienceToken = await getTokenForAudience(
            message.options.audience
          );

          webViewRef.current?.injectJavaScript(`
            window.handleSilentAuthResponse({
              accessToken: '${audienceToken}',
              expiresIn: 3600
            }, null);
          `);
          return;
        }
      }

      // Refresh'i dene
      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 başarısız - login gerekli');
      }
    } 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);

      // Refresh için react-native-app-auth kullan
      const result = await refresh(auth0Config, {
        refreshToken: refreshToken
      });

      // Saklanan token'ları güncelle
      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 başarısız:', error);
      return false;
    }
  };

  return (
    <WebView
      ref={webViewRef}
      source={{ uri: url }}
      injectedJavaScript={injectedJavaScript}
      onMessage={handleWebViewMessage}
      sharedCookiesEnabled={true} // Session paylaşımı için önemli
      thirdPartyCookiesEnabled={true} // Auth0 cookie'leri için
      domStorageEnabled={true} // localStorage için
    />
  );
}

React Native'de Silent Login: Komple Flow#

React Native'de micro frontend'lerle silent login'in uçtan uca nasıl çalıştığı:

TypeScript
// silent-login-flow.ts
class SilentLoginFlow {
  private auth0: Auth0Native;
  private tokenCache: TokenCache;
  private webViewBridge: WebViewBridge;

  async performSilentLogin(): Promise<boolean> {
    // Adım 1: Native token cache'i kontrol et
    const cachedTokens = await this.tokenCache.getTokens();

    if (cachedTokens && !this.isExpired(cachedTokens)) {
      // Valid token'larımız var, WebView bridge'i kur
      await this.setupWebViewBridge(cachedTokens);
      return true;
    }

    // Adım 2: Refresh token var mı kontrol et
    const refreshToken = await this.tokenCache.getRefreshToken();

    if (refreshToken) {
      try {
        // Refresh'i dene
        const newTokens = await this.auth0.refreshTokens(refreshToken);
        await this.tokenCache.storeTokens(newTokens);
        await this.setupWebViewBridge(newTokens);
        return true;
      } catch (error) {
        console.log('Refresh başarısız, Auth0 session deneniyor');
      }
    }

    // Adım 3: Auth0 session'ı kontrol et (SSO)
    try {
      const ssoTokens = await this.checkAuth0Session();
      if (ssoTokens) {
        await this.tokenCache.storeTokens(ssoTokens);
        await this.setupWebViewBridge(ssoTokens);
        return true;
      }
    } catch (error) {
      console.log('Auth0 session bulunamadı');
    }

    // Adım 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 başarısız, explicit login gerekli
  }

  private async checkAuth0Session(): Promise<TokenSet | null> {
    // SSO kontrolü için custom tab / ASWebAuthenticationSession kullan
    const ssoCheckUrl = `https://${AUTH0_DOMAIN}/authorize?` +
      `client_id=${CLIENT_ID}&` +
      `response_type=token&` +
      `redirect_uri=${REDIRECT_URI}&` +
      `scope=openid profile email&` +
      `prompt=none&` + // Silent auth için kritik
      `response_mode=query`;

    try {
      // Bu hidden web session'da açılır
      const result = await InAppBrowser.openAuth(ssoCheckUrl, REDIRECT_URI, {
        ephemeralWebSession: false, // Shared session kullan
        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) {
    // WebView yüklenmeden önce token'ları inject et
    const script = `
      window.__AUTH_TOKENS__ = {
        accessToken: '${tokens.accessToken}',
        idToken: '${tokens.idToken}',
        expiresAt: ${tokens.expiresAt}
      };

      // Auto-renewal kur
      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);
  }
}

Öğrenilen Dersler: Savaş Hikayeleri#

1. Cookie Problemi#

Auth0 session yönetimi için cookie kullanır. React Native WebView'larda third-party cookie'ler genelde engellenir. Çözüm:

TypeScript
// WebView'lar arasında cookie paylaşımını etkinleştir
const cookieManager = require('@react-native-cookies/cookies');

// Auth0 cookie'lerini WebView'lar arasında paylaş
await cookieManager.setFromResponse(
  `https://${AUTH0_DOMAIN}`,
  'auth0_session=...; SameSite=None; Secure'
);

2. Token Boyutu Problemi#

Multiple audience token'ları = büyük localStorage. 10MB limitine takıldık. Çözüm:

TypeScript
// Storage'dan önce token'ları sıkıştır
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. Race Condition#

Birden fazla micro frontend aynı anda token istediğinde race condition oluştu. Çözüm:

TypeScript
class TokenRequestQueue {
  private queue: Map<string, Promise<string>> = new Map();

  async getToken(audience: string): Promise<string> {
    // Zaten fetch ediliyorsa, mevcut promise'i döndür
    const existing = this.queue.get(audience);
    if (existing) return existing;

    // Yeni fetch promise'i oluştur
    const fetchPromise = this.fetchToken(audience);
    this.queue.set(audience, fetchPromise);

    try {
      const token = await fetchPromise;
      return token;
    } finally {
      // Resolution'dan sonra temizle
      this.queue.delete(audience);
    }
  }
}

Güvenlik Konuları#

  1. Token Storage: Token'ları asla plain text olarak saklama. Encrypted storage kullan:
TypeScript
import * as Keychain from 'react-native-keychain';

// Token'ları güvenli sakla
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 Güvenliği: Tüm mesajları validate et:
TypeScript
const validateWebViewMessage = (event: any): boolean => {
  // İzin verilen origin'leri whitelist'le
  const allowedOrigins = [
    'https://billing.myapp.com',
    'https://dashboard.myapp.com'
  ];

  if (!allowedOrigins.includes(event.origin)) {
    console.error('Geçersiz origin:', event.origin);
    return false;
  }

  // Mesaj yapısını validate et
  if (!event.data || typeof event.data !== 'object') {
    return false;
  }

  // Mesaj imzasını validate et (implement edilmişse)
  if (!verifyMessageSignature(event.data)) {
    return false;
  }

  return true;
};

Sonuç#

Micro frontend'lerde multi-audience authentication karmaşık ama çözülebilir. Anahtar içgörüler:

  1. Bir login, birden fazla token: Farklı audience'lar için token almak için silent auth kullan
  2. Merkezileştirilmiş token yönetimi: Shell application orkestre etsin
  3. Mesaj tabanlı iletişim: Cross-domain için postMessage ve BroadcastChannel kullan
  4. React Native için native bridge: WebView'larda Auth0 client'ı override et
  5. Agresif caching: Ama akıllı refresh stratejileriyle

Bu setup şimdi micro frontend'lerimizde ayda 50+ milyon authentication'ı 99.9% silent authentication başarı oranıyla handle ediyor.

Unutma: Karmaşıklık edge case'lerde. Expired token'lar, network hataları ve concurrent request'lerle test et. Gelecekteki ben'in teşekkür edecek.

Loading...

Yorumlar (0)

Sohbete katıl

Düşüncelerini paylaşmak ve toplulukla etkileşim kurmak için giriş yap

Henüz yorum yok

Bu yazı hakkında ilk düşüncelerini paylaşan sen ol!

Related Posts