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#
// 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
},
'*'
);
});
}
}
Multiple Audience'lar için Auth0 Rules#
Auth0 Rules multi-audience senaryoları için özel handling gerektirir:
// 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:
// 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);
}
}
Öğ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:
// 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);
}
}
}
Güvenlik Konuları#
- 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
}
);
- 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;
};
Sonuç#
Micro frontend'lerde multi-audience authentication karmaşık ama çözülebilir. Anahtar içgörüler:
- Bir login, birden fazla token: Farklı audience'lar için token almak için silent auth kullan
- Merkezileştirilmiş token yönetimi: Shell application orkestre etsin
- Mesaj tabanlı iletişim: Cross-domain için postMessage ve BroadcastChannel kullan
- React Native için native bridge: WebView'larda Auth0 client'ı override et
- 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.
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!
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!