İçeriğe atla

2025-09-04

Micro Frontend'lerde Multi-Audience Auth0: Token Yönetimi Kalıpları ve Implementasyon

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

Özet

Auth0 multi-audience authentication’ı dağıtık micro frontend’ler arasında uygulamak, özellikle hem web hem de React Native WebView ortamlarını desteklerken benzersiz zorluklar sunar. Bu vaka çalışması, tek login akışında birden fazla API audience’ını destekleyen birleşik token yönetim sistemi için etkili kalıpları belgeler.

Ele Alınan Temel Zorluklar:

  • Micro frontend’ler arasında cross-domain token paylaşımı
  • Auth0 ile multi-audience JWT token yönetimi
  • React Native WebView’larda silent authentication
  • Dağıtık uygulamalar arasında token refresh koordinasyonu

Çözüm Özeti: Mesaj tabanlı iletişim, silent authentication akışları ve güvenliği korurken birden fazla login gereksinimini ortadan kaldıran native-web köprüleri ile merkezileştirilmiş token yöneticisi. Token refresh koordinasyonu shell uygulamasında merkezi tutulur; micro frontend’ler sadece mevcut token’ı alır.

Problem Tanımı

Micro frontend migrasyonumuz kritik bir authentication darboğazını ortaya çıkardı. Mimari şunları gerektiriyordu:

Bu dağıtık sistem mimarisini düşünün:

React Native

Backend API'ler

Micro Frontend'ler

Shell Application

Kullanıcı

Shell App

auth.myapp.com

Token Manager

Billing MFE

billing.myapp.com

Dashboard MFE

dashboard.myapp.com

Analytics MFE

analytics.myapp.com

Billing API

Audience: billing-api

Core API

Audience: core-api

Analytics API

Audience: analytics-api

React Native App

WebView: Billing

WebView: Dashboard

Temel Problem: Her API, JWT token’da farklı bir audience gerektirir, ancak Auth0’nun standart akışı authentication başına yalnızca bir audience’ı destekler. Bu, kullanıcıları üç kez ayrı ayrı authenticate etmeye zorlayacaktır. Tek sign-on deneyimini korumak için alternatif token stratejileri gerekiyordu.

Ek Karmaşıklık: Sistemin React Native WebView’larda sorunsuz çalışması gerekiyordu; burada geleneksel web authentication kalıpları cookie kısıtlamaları ve cross-origin sınırlamaları nedeniyle bozulur.

Başarısız Yaklaşımlar

Çalışan çözümümüze ulaşmadan önce, gereksinimlerimizi karşılamayan birkaç yaklaşımı keşfettik:

Yaklaşım 1: Birden Fazla Auth0 Application

Konsept: Her micro frontend için ayrı Auth0 application’ları oluştur. Neden başarısız oldu: Kullanıcılar yine birden fazla login yapmak zorundaydı ve application konfigürasyonlarını yönetmek hantal hale geldi. Cross-application session paylaşımı güvenilmez oldu.

Yaklaşım 2: Permission Scoping ile Tek Audience

Konsept: API erişimini kontrol etmek için ince taneli scope’lar ile bir audience kullan. Neden başarısız oldu: API’ler audience-spesifik permission’ları düzgün validate edemedi ve takımlar arasında scope yönetimi karmaşık hale geldi.

Yaklaşım 3: Server-Side Token Exchange

Konsept: Farklı audience’lar için token’ları server-side’da exchange et. Neden başarısız oldu: Önemli gecikme ekledi, tüm servislerde backend değişiklikleri gerektirdi ve React Native implementasyonunu karmaşıklaştırdı.

Çalışan Çözüm: Koordineli Multi-Audience Token Yönetimi

Başarılı yaklaşımımız, tüm micro frontend’ler için authentication’ı orkestre eden shell application’a odaklanır:

1. Token Manager Mimarisi

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

// 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ü

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

// 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
        },
        '*'
      );
    });
  }
}

Multi-Audience Desteği için Auth0 Actions

Önemli: Auth0 Rules artık kullanımdan kaldırıldı. Multi-audience senaryoları için Actions kullanın:

// auth0-action.js - Actions kullanarak tüm audience'lar için custom claim'ler ekle
exports.onExecutePostLogin = async (event, api) => {
  const { user, request } = event;

  // 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 = request.query?.audience || request.body?.audience;

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

  // Tüm token'lara user metadata ekle
  api.accessToken.setCustomClaim(namespace + 'email', user.email);
  api.accessToken.setCustomClaim(namespace + 'roles', user.app_metadata?.roles || []);

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

  // Sadece primary audience için refresh token göstergesi ekle
  if (requestedAudience === 'https://api.myapp.com/core') {
    api.accessToken.setCustomClaim(namespace + 'can_refresh', true);
  }
};

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:

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

// 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ığı:

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

Multi-Resource Refresh Token’lar (MRRT)

Auth0 multi-audience senaryoları için Multi-Resource Refresh Token’ları tanıttı. Tek bir refresh token ile birden fazla audience için access token alınabilir. MRRTTokenManager ile refreshMultipleAudiences pattern’i kullanılabilir.

Güvenlik Konuları

Kritik Uyarı: JWT token’ları her zaman backend’de validate edin. Asla sadece client-side token validation’a güvenmeyin.

// Backend JWT validation örneği
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');

const client = jwksClient({
  jwksUri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`
});

function getKey(header, callback) {
  client.getSigningKey(header.kid, (err, key) => {
    const signingKey = key.publicKey || key.rsaPublicKey;
    callback(null, signingKey);
  });
}

// Token'ı validate et
const verifyToken = (token, audience) => {
  return new Promise((resolve, reject) => {
    jwt.verify(token, getKey, {
      audience: audience,
      issuer: `https://${AUTH0_DOMAIN}/`,
      algorithms: ['RS256']
    }, (err, decoded) => {
      if (err) reject(err);
      else resolve(decoded);
    });
  });
};

Ek Güvenlik Önlemleri:

  1. Token Depolama: Token’lar için güvenli, şifrelenmiş depolama kullanın
  2. Origin Validation: postMessage handler’larda her zaman mesaj origin’lerini validate edin
  3. Sadece HTTPS: Token’ları asla şifrelenmemiş bağlantılar üzerinden iletmeyin
  4. Token Rotation: Uygun refresh token rotation uygulayın
  5. Audience Validation: Audience claim’lerinin beklenen değerlerle eşleştiğini doğrulayın

Implementasyon Dersleri

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

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

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

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

React Native Güvenlik Implementasyonu

  1. Token Storage: Token’ları asla plain text olarak saklama. Encrypted storage kullan:
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:
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;
};

Temel Çıkarımlar

Micro frontend’lerde multi-audience authentication karmaşık ama doğru mimariyle çözülebilir:

  1. Önce Mimari: Token yönetimini baştan merkezileştirilmiş bir servis olarak tasarlayın
  2. Edge Case’leri Test Edin: Süresi dolmuş token’lar, network hataları ve race condition’lar gerçek karmaşıklığı ortaya çıkarır
  3. Güvenlik Odaklı Tasarım: Uygun JWT validation ve güvenli token depolama uygulayın
  4. Progressive Enhancement: Önce web için geliştirin, sonra React Native WebView’lar için adapte edin
  5. Mesaj Kontratları: Bileşenler arasında açık iletişim protokolleri tanımlayın

Bu yaklaşım, production ortamlarında tutarlı silent authentication başarı oranlarıyla yüksek hacimli authentication yüklerini işleyerek güvenilir olduğunu kanıtlamıştır.

Implementasyon, karmaşık authentication senaryolarının dikkatli mimari ve güvenlik temellerine odaklanarak çözülebileceğini göstermektedir. Implementasyon detaylarına dalmadan önce token akış tasarımına odaklanın.

İlgili yazılar

Güvenlik Sözlüğü: Her Geliştirici Ekibinin Bilmesi Gereken 50+ Terim

Prodüksiyon sistemleri deneyiminden implementasyon bağlamı, öğrenilen dersler ve pratik rehberlik içeren kapsamlı güvenlik referansı.

securityauthenticationoauth2+9
İş Alanlarına Göre Kimlik Doğrulama ve Yetkilendirme Stratejileri: Bankacılık Güvenliği Sosyal Medya Kaosuyla Buluştuğunda

Farklı sektörlerde auth sistemleri geliştirdikten sonra öğrendim ki tek boyutlu kimlik doğrulama bir efsane. Her iş alanının kendine özgü gereksinimleri var ve bu da auth mimarinizi dramatik şekilde etkiliyor.

authenticationauthorizationsecurity+8
Mobil, Web ve API için Kimlik Doğrulama Sağlayıcıları: Doğru Çözümü Seçmek için Eksiksiz Kılavuz

Auth0, Firebase Auth, Supabase Auth, AWS Cognito ve özel çözümlerin gerçek dünya karşılaştırması. Her birini ne zaman kullanmalı, maliyet analizi ve bana her şeyi öğreten hata ayıklama kabusları.

auth0cognitofirebase+2
React Native'de Auth0 ve Biyometrik Kimlik Doğrulama ile Gerçek Dünya Session Yönetimi

Production React Native uygulamalarında Auth0, biyometrik kimlik doğrulama ve düzgün token yaşam döngüsü yönetimi ile güvenli oturum yönetimi uygulamak için adım adım kılavuz

auth0biometricsreact-native+1
Serverless Framework'ten AWS CDK'ya Geçiş: Bölüm 5 - Authentication, Authorization ve IAM

Serverless Framework'ten AWS CDK'ya geçerken Cognito ile güçlü kimlik doğrulama, API Gateway authorizer'lar ve ince taneli IAM politikaları uygulama.

authorizationaws-cdkcognito+3