Auth0 ile Çoklu Audience: Micro Frontend'lerde Token Yönetimi
Micro frontend'lerde Auth0 multi-audience authentication: token yönetim stratejileri ve React Native'de WebView tabanlı uygulamalarla silent authentication.
Auth0 access token’ı tek bir audience taşır. Üç ayrı API ile konuşan bir micro frontend kümesi bu yüzden üç token’a ihtiyaç duyar ve kısıtın ilk okunuşu “üç kez login” der. Gerek yok. Auth0 session’ını shell application’da tek noktada tutun. Shell, bu session’ı istendiğinde audience başına access token’a çevirir ve token’ları origin’i doğrulanmış bir postMessage kanalıyla micro frontend’lere dağıtır. Diğer düzenlemeler (micro frontend başına ayrı Auth0 application, ince taneli scope’larla tek geniş audience, sunucu tarafında token değişimi) daha pahalıya mal olur ve daha azını verir.
Tarayıcı tarafı işin kolay yarısı. İş React Native’e gelince zorlaşır: aynı micro frontend’ler WebView içinde çalışır ve WebView, Auth0 session cookie’sine güvenilir biçimde erişemez. Bu yüzden token’ların sahibi native katman olur, WebView da token’ı ondan ister.
Tek Audience Kısıtı#
Bu dağıtık sistem mimarisini düşünün:
Her API farklı bir aud claim’i doğrular ve tek bir /authorize turu yalnızca tek audience için token üretir. Billing API için üretilmiş bir token’ı analytics API reddeder, tersi de geçerlidir. Koordinasyon katmanı olmadan bu, API başına bir interaktif login demektir.
Sistemin ayrıca React Native WebView’lar içinde çalışması gerekir; orada alışıldık web kalıpları cookie kısıtları ve cross-origin sınırları yüzünden bozulur.
Alternatifler ve Sınırları#
Kağıt üzerinde makul görünen üç düzenleme, pratikte shell broker’dan daha pahalıya çıkar:
Micro frontend başına ayrı Auth0 application#
Fikir: her micro frontend kendi Auth0 application’ına ve audience’ına sahip olur. Sınırı: kullanıcı yine application başına authenticate olur ve konfigürasyon farkları sistemin kendisinden hızlı büyür. Application’lar arası session paylaşımı, kontrol etmediğiniz cookie davranışına bağlıdır.
İnce taneli scope’larla tek audience#
Fikir: tüm API’leri kapsayan tek bir audience, erişimi scope’lar belirler.
Sınırı: aud artık API’leri birbirinden ayırmadığı için her API, başka bir API’ye ait token’ı reddetme yeteneğini kaybeder. Scope sahipliği takımlar arasında bulanıklaşır.
Sunucu tarafında token değişimi#
Fikir: bir backend servisi gelen token’ı audience’a özel token’la takas eder. Sınırı: her audience geçişine bir network hop ekler, onu tüketen her serviste değişiklik ister ve Auth0 session’ının zaten yaptığı işi tekrarlar.
Koordineli Multi-Audience Token Yönetimi#
Auth0 client’ının sahibi shell application’dır; authentication’ı tüm micro frontend’ler adına o yürütür:
1. Token Manager Mimarisi#
// token-manager.ts - Token yönetiminin çekirdeği
import { Auth0Client } from '@auth0/auth0-spa-js'; // ^2.1.3
interface TokenSet {
accessToken: string;
expiresAt: number;
audience: string;
scope: string;
}
class MultiAudienceTokenManager {
private tokens: Map<string, TokenSet> = new Map();
// Yalnızca refresh token'ı elinde tutan React Native köprüsü doldurur.
// Tarayıcıda SPA SDK'sı token'ı dışarı vermez, bu alan null kalır.
private primaryRefreshToken: string | null = null;
private auth0Client: Auth0Client;
constructor(private config: Auth0Config) {
this.auth0Client = new Auth0Client({
domain: config.domain,
clientId: config.clientId,
cacheLocation: 'memory', // Micro frontend'ler için kritik
useRefreshTokens: true,
authorizeTimeoutInSeconds: 60
});
}
// Sistemin ihtiyaç duyduğu tüm scope'ları isteyen tek interaktif login.
// Sayfadan ayrılır; akış callback rotasında completeLogin() ile devam eder.
async startLogin(primaryAudience: string): Promise<void> {
await this.auth0Client.loginWithRedirect({
authorizationParams: {
audience: primaryAudience,
scope: this.getAllRequiredScopes(),
redirect_uri: window.location.origin
}
});
}
// Callback rotasında çalışır, ardından kalan audience'ları ısıtır
async completeLogin(audiences: string[]): Promise<void> {
await this.auth0Client.handleRedirectCallback();
for (const audience of audiences) {
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;
}
const scope = this.getScopeForAudience(audience);
try {
// SDK mevcut session'ı yeni audience için tekrar kullanır
const result = await this.auth0Client.getTokenSilently({
authorizationParams: { audience, scope },
detailedResponse: true // Düz string yerine expires_in de döner
});
this.storeToken(audience, {
accessToken: result.access_token,
expiresAt: Date.now() + result.expires_in * 1000,
audience,
scope
});
return result.access_token;
} catch (error) {
// Session yoksa veya third-party cookie engelliyse silent auth düşer
if (this.primaryRefreshToken) {
return this.refreshTokenForAudience(audience);
}
throw error;
}
}
// Doğrudan refresh_token grant'i: refresh token'ın sahibi uygulamanın
// kendisi olduğu durum için. Yani React Native köprüsü, tarayıcı değil.
private async refreshTokenForAudience(audience: string): Promise<string> {
const scope = this.getScopeForAudience(audience);
const response = await fetch(`https://${this.config.domain}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: this.config.clientId,
refresh_token: this.primaryRefreshToken,
audience,
scope
})
});
if (!response.ok) {
throw new Error(`${audience} için refresh başarısız: ${response.status}`);
}
const data = await response.json();
this.storeToken(audience, {
accessToken: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
audience,
scope: data.scope
});
return data.access_token;
}
}
2. Micro Frontend’ler için Cross-Domain Token Paylaşımı#
Zor kısım, token’ları farklı subdomain’ler arasında paylaşmak. Sırayla denenen üç fallback:
// 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(auth0Config));
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);
// Tek interaktif login; tarayıcı burada sayfadan ayrılır
await this.tokenManager.startLogin(audiences[0]);
}
// Auth0 geri yönlendirdikten sonra callback rotasından çağrılır
async finishLogin(audiences: string[]) {
await this.tokenManager.completeLogin(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#
Refresh burada zor, çünkü aynı token’ın süresinin dolmak üzere olduğunu birden fazla frame aynı anda fark edebilir. Tek koordinatör, audience başına tek aktif promise:
// token-refresh-coordinator.ts
import { jwtDecode } from 'jwt-decode'; // ^4.0.0 default export'u kaldırdı
class TokenRefreshCoordinator {
private refreshPromises: Map<string, Promise<string>> = new Map();
private refreshTimers: Map<string, ReturnType<typeof setTimeout>> = 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. v1'deki ignoreCache'in yerini cacheMode aldı.
const token = await auth0Client.getTokenSilently({
authorizationParams: { audience },
cacheMode: 'off'
});
// Expiry almak için decode et
const decoded = jwtDecode<{ exp: number }>(token);
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(`${audience} için token refresh başarısız:`, 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 bildir. Token'ı asla targetOrigin '*' ile göndermeyin.
document.querySelectorAll('iframe').forEach(iframe => {
const target = new URL(iframe.src, window.location.href).origin;
if (!ALLOWED_MFE_ORIGINS.includes(target)) return;
iframe.contentWindow?.postMessage(
{ type: 'TOKEN_REFRESHED', audience, token },
target
);
});
}
}
Multi-Audience Desteği için Auth0 Actions#
Rules ve Hooks, Actions lehine kullanımdan kaldırıldı; yeni tenant’larda yalnızca Actions var. Audience’a özel claim’lerin yeri post-login Action’ıdır:
// 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);
}
};
Tarayıcıda Silent Authentication#
Tek session’ı birden çok audience token’ına çeviren mekanizma, gizli bir iframe içindeki prompt=none. @auth0/auth0-spa-js bunu zaten içeride yapıyor, o yüzden önce SDK’ya bakın. Elle yazılmış hali okumaya değer, çünkü sonunda debug edeceğiniz hata durumları burada görünür:
// silent-auth-handler.ts
class SilentAuthHandler {
private iframe: HTMLIFrameElement | null = null;
private pendingVerifier: string | 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 = await 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, options)
.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 async buildAuthUrl(options: SilentAuthOptions): Promise<string> {
// Authorization code + PKCE. Güncel Auth0 application'larında implicit
// grant varsayılan olarak kapalı; response_type=token reddedilir.
this.pendingVerifier = this.createCodeVerifier();
const params = new URLSearchParams({
client_id: AUTH0_CLIENT_ID,
response_type: 'code',
code_challenge: await this.sha256UrlSafe(this.pendingVerifier),
code_challenge_method: 'S256',
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,
options: SilentAuthOptions
): Promise<TokenSet> {
// Code'a dokunmadan önce state'i doğrula
if (!this.validateState(response.state)) {
throw new Error('State validation failed');
}
// Authorization code'u token ile takas et
const res = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'authorization_code',
client_id: AUTH0_CLIENT_ID,
code: response.code,
code_verifier: this.pendingVerifier,
redirect_uri: `${window.location.origin}/silent-callback.html`
})
});
if (!res.ok) {
throw new Error(`Code takası başarısız: ${res.status}`);
}
const data = await res.json();
return {
accessToken: data.access_token,
expiresAt: Date.now() + data.expires_in * 1000,
audience: options.audience,
scope: data.scope
};
}
private cleanup() {
if (this.iframe && this.iframe.parentNode) {
this.iframe.parentNode.removeChild(this.iframe);
this.iframe = null;
}
}
}
React Native’de WebView Micro Frontend’ler#
React Native içinde aynı micro frontend’ler WebView’da açılır ve WebView, native katmanın az önce kurduğu Auth0 session cookie’sini gördüğüne güvenemez. Bu yüzden token’ların sahibi native taraf olur; WebView da onları mesaj köprüsü üzerinden ister:
// react-native-auth-bridge.tsx
import React, { useRef, useState } from 'react';
import { WebView } from 'react-native-webview'; // ^13.8.6
import AsyncStorage from '@react-native-async-storage/async-storage'; // ^1.23.1
import { authorize, refresh } from 'react-native-app-auth'; // ^7.1.0
interface AuthBridge {
webViewRef: React.RefObject<WebView>;
tokens: Map<string, string>;
}
export function AuthenticatedMicroFrontend({ url, audience }: Props) {
const webViewRef = useRef<WebView>(null);
const [tokens, setTokens] = useState<Map<string, string>>(new Map());
// react-native-app-auth config: Auth0 uyumluluğu ve iOS/Android tarafındaki
// native authentication session desteği için tercih edildi
const auth0Config = {
issuer: `https://${AUTH0_DOMAIN}`,
clientId: AUTH0_CLIENT_ID,
redirectUrl: 'com.myapp://auth/callback',
scopes: ['openid', 'profile', 'email', 'offline_access'],
additionalParameters: {
audience: audience
}
};
// 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);
setTokens(prev => new Map(prev).set(message.audience, token!));
}
// Tırnak yerine JSON.stringify: içinde tırnak geçen bir değer aksi
// halde string'i kapatır ve WebView içinde kod olarak çalışır.
webViewRef.current?.injectJavaScript(`
window.handleTokenResponse(
${JSON.stringify(message.requestId)},
${JSON.stringify(token)},
null
);
true;
`);
} catch (error) {
// Error'u WebView'a geri gönder
webViewRef.current?.injectJavaScript(`
window.handleTokenResponse(
${JSON.stringify(message.requestId)},
null,
${JSON.stringify((error as Error).message)}
);
true;
`);
}
};
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: ${JSON.stringify(audienceToken)}
}, null);
true;
`);
return;
}
}
// Refresh'i dene
const refreshed = await refreshAuth();
if (refreshed) {
const audienceToken = await getTokenForAudience(
message.options.audience
);
webViewRef.current?.injectJavaScript(`
window.handleSilentAuthResponse({
accessToken: ${JSON.stringify(audienceToken)}
}, null);
true;
`);
} else {
throw new Error('Silent auth başarısız, login gerekli');
}
} catch (error) {
webViewRef.current?.injectJavaScript(`
window.handleSilentAuthResponse(
null,
${JSON.stringify((error as Error).message)}
);
true;
`);
}
};
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 Akışı#
Uygulama login ekranını göstermeden önce sırayla denenen dört fallback:
// 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;
}
return null;
} catch (error) {
return null;
}
}
private async setupWebViewBridge(tokens: TokenSet) {
// WebView yüklenmeden önce token'ları inject et
const script = `
window.__AUTH_TOKENS__ = {
accessToken: ${JSON.stringify(tokens.accessToken)},
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)#
Multi-Resource Refresh Token, application başına tek bir refresh token’ın birden fazla API için access token almasını sağlar; yukarıdaki muhasebenin çoğunu ortadan kaldırır. Uygulanabilirliğini üç kısıt belirler: MRRT yalnızca first-party application’larla çalışır, her access token hâlâ tek bir API’ye scope’lanır ve Management API bir MRRT politikasına dahil edilemez.
// Birden fazla audience için verimli token yenileme (MRRT)
class MRRTTokenManager {
async refreshMultipleAudiences(refreshToken: string, audiences: string[]): Promise<Map<string, TokenSet>> {
const tokens = new Map<string, TokenSet>();
// MRRT, tek bir refresh token ile birden fazla audience için token almayı sağlar
for (const audience of audiences) {
try {
const response = await fetch(`https://${AUTH0_DOMAIN}/oauth/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
grant_type: 'refresh_token',
client_id: CLIENT_ID,
refresh_token: refreshToken,
audience: audience
})
});
const data = await response.json();
tokens.set(audience, {
accessToken: data.access_token,
expiresAt: Date.now() + (data.expires_in * 1000),
audience: audience
});
} catch (error) {
console.error(`${audience} için MRRT yenileme başarısız:`, error);
}
}
return tokens;
}
}
Güvenlik Konuları#
Kritik Uyarı: JWT token’ları her zaman backend’de validate edin. Asla sadece client-side token validation’a güvenmeyin.
// Hata yönetimiyle birlikte backend JWT validation
import jwt from 'jsonwebtoken';
import jwksClient from 'jwks-rsa';
const client = jwksClient({
jwksUri: `https://${AUTH0_DOMAIN}/.well-known/jwks.json`,
cache: true,
cacheMaxAge: 600000 // 10 dakika
});
function getKey(header: any, callback: Function) {
client.getSigningKey(header.kid, (err: Error | null, key: any) => {
if (err) {
callback(err);
return;
}
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
// Token'ı validate et
const verifyToken = (token: string, audience: string): Promise<any> => {
return new Promise((resolve, reject) => {
jwt.verify(token, getKey, {
audience: audience,
issuer: `https://${AUTH0_DOMAIN}/`,
algorithms: ['RS256']
}, (err: Error | null, decoded: any) => {
if (err) {
console.error('JWT doğrulaması başarısız:', err.message);
reject(err);
} else {
resolve(decoded);
}
});
});
};
Temel Güvenlik Önlemleri:
- Token Depolama: Token’lar için güvenli, şifrelenmiş depolama kullanın
- Origin Validation: postMessage handler’larda her zaman mesaj origin’lerini validate edin
- Sadece HTTPS: Token’ları asla şifrelenmemiş bağlantılar üzerinden iletmeyin
- Token Rotation: Uygun refresh token rotation uygulayın
- Audience Validation: Audience claim’lerinin beklenen değerlerle eşleştiğini doğrulayın
Uygulamada Çıkan Tuzaklar#
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. Race Condition#
Birden fazla micro frontend aynı audience’ı aynı anda ister ve her biri kendi network çağrısını başlatır. Hepsini tek promise üzerinde toplayın:
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 Tarafında Güvenlik#
- Güvenli Token Depolama: Token’ları asla düz metin olarak saklamayın. Şifreli depolama kullanın:
import * as Keychain from 'react-native-keychain';
// Token'ları biyometrik korumayla sakla
const storeTokensSecurely = async (tokens: TokenSet) => {
try {
await Keychain.setInternetCredentials(
'auth.myapp.com',
'tokens',
JSON.stringify(tokens),
{
accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
service: 'myapp-auth' // Benzersiz servis tanımlayıcısı
}
);
} catch (error) {
console.error('Güvenli depolama başarısız:', error);
throw new Error('Authentication token\'ları güvenli biçimde saklanamadı');
}
};
- Mesaj Doğrulamasıyla WebView Güvenliği:
const validateWebViewMessage = (event: any): boolean => {
// İzin verilen origin'leri whitelist'le
const allowedOrigins = [
'https://billing.myapp.com',
'https://dashboard.myapp.com',
'https://analytics.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') {
console.error('Geçersiz mesaj yapısı');
return false;
}
// Zorunlu alanları kontrol et
const requiredFields = ['type', 'requestId'];
for (const field of requiredFields) {
if (!event.data[field]) {
console.error(`Eksik zorunlu alan: ${field}`);
return false;
}
}
return true;
};
Bu Kalıp Nerede İşe Yarar#
Shell’de tek session, origin’i doğrulanmış kanal üzerinden audience başına token ve WebView’lar için native köprü: micro frontend’ler first-party olduğu, kontrol ettiğiniz bir parent frame’i ya da ortak bir kayıtlı alan adını paylaştığı ve login sırasında tüm audience’ları ısıtmak ucuz kaldığı sürece bu kurulum yeterlidir. Application’lar first-party ise MRRT bunun üzerine refresh muhasebesinin çoğunu ortadan kaldırır.
Bu koşullardan biri bozulduğunda kalıbı değiştirin. İş ortağına ait frontend’ler kendi Auth0 application’ları ve kendi consent ekranlarıyla yaşamalı. Onlarca audience’ı olan bir sistemde token’ları login’de toplu ısıtmak yerine route başına tembel almak gerekir. Bir micro frontend kendi backend-for-frontend’ine erişebiliyorsa, token’ları tarayıcıda dolaştırmak yerine o BFF’e karşı httpOnly cookie session kullanmak daha basittir.
Kaynaklar#
- Auth0 Erişim Token’ları (yeni sekmede açılır) - Erişim token’ı yapısı, kapsamlar ve hedef kitleler üzerine resmi belgeler
- Erişim Token’ı Alma - Auth0 Belgeleri (yeni sekmede açılır) - Çok kitleli senaryolar dahil token istek kalıpları
- JSON Web Token’ları - Auth0 Belgeleri (yeni sekmede açılır) - JWT yapısı, claim’ler ve doğrulama en iyi uygulamaları
- OAuth 2.0 Yetkilendirme Çerçevesi - RFC 6749 (yeni sekmede açılır) - Token izinlerini ve istemci yetkilendirme akışlarını tanımlayan IETF standardı
- Auth0 React Native SDK Hızlı Başlangıç (yeni sekmede açılır) - WebView desteğiyle React Native’de Auth0 entegrasyon rehberi
- Multi-Resource Refresh Token - Auth0 Belgeleri (yeni sekmede açılır) - MRRT davranışı ve first-party application sınırı
- Silent Authentication Yapılandırması - Auth0 Belgeleri (yeni sekmede açılır) -
prompt=noneakışı ve session yokken dönen hatalar - JSON Web Token Claim’leri - Auth0 Belgeleri (yeni sekmede açılır) - Çok kiracılı token tasarımı için standart ve özel claim referansı
- Kimlik Doğrulama Akışları - Amazon Cognito (yeni sekmede açılır) - Cognito token kalıplarıyla karşılaştırma için referans
İlgili yazılar
Ekiplerin tartıştığı authentication, token, erişim kontrolü ve Zero Trust terimleri için tanımlar, implementasyon bağlamı ve varsayılanlar.
security · authentication · oauth2 +2
Tek tip auth bir efsane: bankacılık, sağlık, e-ticaret ve SaaS kimlik doğrulama mimarisini birbirinden farklı biçimlendirir.
authentication · authorization · security +5
E-posta, şifre ve SMS OTP kullanan SaaS ekipleri için Cognito passkey geçiş rehberi: yapılandırma, enrollment ve SMS'in yerini alan dört basamaklı recovery merdiveni.
authentication · security · aws +2
Webhook, imzalı URL ve servisler arası kimlik doğrulama için pratik bir HMAC-SHA-256 rehberi: üç dilde çalışan kod ve dijital imzaya geçmenin gerektiği sınır.
security · encryption · webhooks +4
AWS Cognito ve Verified Permissions ile SaaS yetkilendirmeyi Cedar politikaları, çok kiracılı desenler, JWT akışı ve maliyet analiziyle TypeScript'te kurun.
authorization · aws · authentication +4