React Native'de Auth0 ve Biyometrik Kimlik Doğrulama ile Gerçek Dünya Session Yönetimi
Üretim deneyimlerine dayalı olarak React Native uygulamalarında mobil oturum yönetimi zorlukları, Auth0 entegrasyonu, biyometrik kimlik doğrulama ve token yaşam döngüsü yönetimi
Geçen hafta, production'daki mobil uygulamamız öyle bir sorunla karşılaştı ki, session yönetimi hakkında bildiğimi sandığım her şeyin yanlış olduğunu öğrendim. Kullanıcılar rastgele logout oluyordu, biyometrik kimlik doğrulama aralıklarla çalışmıyordu ve arka plan bildirimlerimiz yaklaşık 20 dakika sonra duruyordu. Tanıdık geliyor mu?
React Native uygulamamızdaki Auth0 entegrasyonunu üç gün boyunca debug ettikten ve sayısız kahve içtikten sonra, nihayet access token'lar, refresh token'lar ve mobil işletim sistemi kısıtlamaları arasındaki karmaşık dansı anladım. Zor yoldan öğrendiklerimi paylaşayım.
Her Şeyi Başlatan Session Yönetimi Kabusumuz#
Şunu hayal edin: React Native uygulamanıza Auth0'ı yeni entegre ettiniz. Development'ta her şey mükemmel çalışıyor. Kullanıcılar giriş yapabiliyor, Face ID sihir gibi çalışıyor ve API çağrılarınız kimlik doğrulamalı. Production'a deploy ediyorsunuz ve sonra öfkeli e-postalar gelmeye başlıyor.
"Neden uygulamayı her açtığımda giriş yapmam gerekiyor?" "Güncellemeden sonra Face ID çalışmayı durdurdu!" "Uygulama kapalıyken bildirim almıyorum!"
Yaşadınız mı? Hadi bu sefer düzgün çözelim.
Session Yönetimi 101: Önce Bilmeniz Gerekenler#
Teknik implementasyona dalmadan önce, session yönetiminin mobil ortamda ne anlama geldiğini açıklayalım. Kimlik doğrulama sistemlerine yeniyseniz, şöyle düşünün: session yönetimi, bir ofis binasında kimliğinizi doğrulayıp hangi odalara girebileceğinize karar veren bir güvenlik görevlisi gibidir.
Kullanıcı Durumlarını Anlamak#
Kimlik doğrulamalı herhangi bir mobil uygulamada, kullanıcınız herhangi bir zamanda birkaç durumdan birinde bulunur:
// types/AuthState.ts
export enum UserState {
// Kullanıcı hiç giriş yapmamış veya çıkış yapmış
UNAUTHENTICATED = 'unauthenticated',
// Kullanıcı giriş yapma sürecinde
AUTHENTICATING = 'authenticating',
// Kullanıcı geçerli token'larla tam olarak kimlik doğrulamalı
AUTHENTICATED = 'authenticated',
// Kullanıcı kimlik doğrulamalıydı ama token'ların süresi dolmuş
SESSION_EXPIRED = 'session_expired',
// Kullanıcı mevcut ama ek doğrulama gerektiriyor (biyometrik, 2FA)
REQUIRES_VERIFICATION = 'requires_verification',
// Uygulama başlangıçta saklanan kimlik bilgilerini kontrol ediyor
CHECKING_AUTH = 'checking_auth',
// Kullanıcı çıkış yapıyor
LOGGING_OUT = 'logging_out'
}
export interface AuthState {
userState: UserState;
user: User | null;
tokens: {
accessToken: string | null;
refreshToken: string | null;
expiresAt: number | null;
};
lastActivity: number;
biometricEnabled: boolean;
sessionStartTime: number;
}
Token Ekosistemi: Dijital Anahtarlarınız#
Token'ları farklı türde anahtarlar olarak düşünün:
-
Access Token: Geçici ziyaretçi kartı gibi
- Kısa ömürlü (tipik olarak 15 dakika)
- API kaynaklarınıza erişim sağlar
- Her API isteğiyle birlikte gönderilmeli
- Süresi dolduğunda yenisine ihtiyaç var
-
Refresh Token: Ziyaretçi kartı üretebilen ana anahtar gibi
- Uzun ömürlü (mobil uygulamalar için 30 gün)
- Sadece yeni access token'lar almak için kullanılır
- API'nıza asla gönderilmemeli
- Bunun süresi dolduğunda kullanıcı tekrar giriş yapmalı
-
ID Token: Çalışan kimlik kartınız gibi
- Kullanıcı bilgilerini içerir (isim, e-posta, vb.)
- Kullanıcı profil verilerini görüntülemek için kullanılır
- API yetkilendirmesi için kullanılmamalı
Giriş Sırasında Ne Olur?#
Kullanıcı giriş yaptığında adım adım yolculuk:
// auth/AuthFlow.ts
class AuthFlow {
async performLogin(email: string, password: string): Promise<AuthResult> {
// Adım 1: Yükleniyor göstermek için durumu güncelle
this.updateState({ userState: UserState.AUTHENTICATING });
try {
// Adım 2: Kimlik bilgilerini Auth0'a gönder
const authResponse = await auth0.authenticate({
username: email,
password: password,
scope: 'openid profile email offline_access'
});
// Adım 3: Yanıttan token'ları çıkar
const tokens = {
accessToken: authResponse.accessToken,
refreshToken: authResponse.refreshToken,
expiresAt: Date.now() + (authResponse.expiresIn * 1000)
};
// Adım 4: Token'ları güvenli şekilde sakla
await this.secureStorage.storeTokens(tokens);
// Adım 5: Kullanıcı profil bilgilerini al
const userProfile = await this.fetchUserProfile(tokens.accessToken);
// Adım 6: Uygulama durumunu güncelle
this.updateState({
userState: UserState.AUTHENTICATED,
user: userProfile,
tokens,
sessionStartTime: Date.now(),
lastActivity: Date.now()
});
// Adım 7: Otomatik token yenilemeyi ayarla
await this.scheduleTokenRefresh(tokens.expiresAt);
// Adım 8: Mevcut ise biyometrik kimlik doğrulamayı yapılandır
if (await this.biometrics.isAvailable()) {
await this.setupBiometricAuth(tokens);
}
return { success: true };
} catch (error) {
// Adım 9: Giriş başarısızlığını ele al
this.updateState({ userState: UserState.UNAUTHENTICATED });
throw error;
}
}
}
Çıkış Sırasında Ne Olur?#
Çıkış, düşündüğünüzden daha karmaşıktır. Olması gereken:
// auth/LogoutFlow.ts
class LogoutFlow {
async performLogout(reason: LogoutReason): Promise<void> {
// Adım 1: Yeni işlemleri önlemek için durumu güncelle
this.updateState({ userState: UserState.LOGGING_OUT });
try {
// Adım 2: Devam eden arka plan işlemlerini iptal et
await this.cancelBackgroundTasks();
// Adım 3: Token yenileme timer'larını durdur
this.tokenManager.stopAllRefreshTimers();
// Adım 4: Token'ları bellekten temizle
this.clearMemoryTokens();
// Adım 5: Token'ları güvenli depolamadan kaldır
await this.secureStorage.clearAllTokens();
// Adım 6: Kullanıcı verisi cache'ini temizle
await this.clearUserDataCache();
// Adım 7: Auth0'la token'ları iptal et (kullanıcı başlattıysa)
if (reason === 'user_initiated') {
try {
await this.revokeTokensWithAuth0();
} catch (error) {
// İptal başarısız olsa da çıkışı başarısız yapma
console.warn('Token iptali başarısız:', error);
}
}
// Adım 8: Biyometrik saklanan kimlik bilgilerini temizle
await this.biometrics.clearStoredCredentials();
// Adım 9: Push bildirimlerden kaydı sil
await this.pushNotifications.unregister();
// Adım 10: Navigasyon stack'ini temizle ve yönlendir
this.navigation.resetToLogin();
// Adım 11: Son durumu güncelle
this.updateState({
userState: UserState.UNAUTHENTICATED,
user: null,
tokens: { accessToken: null, refreshToken: null, expiresAt: null },
lastActivity: 0,
sessionStartTime: 0
});
// Adım 12: Analytics event'i logla
this.analytics.track('user_logged_out', { reason });
} catch (error) {
console.error('Çıkış hatası:', error);
// Bir şey başarısız olsa bile, kullanıcıyı kimlik doğrulamasız duruma zorla
this.forceUnauthenticatedState();
}
}
}
Session Süresi Dolması: İşler Ters Gittiğinde#
Session süresi dolması birkaç şekilde olabilir ve her biri farklı ele alım gerektirir:
// auth/SessionExpiryHandler.ts
class SessionExpiryHandler {
async handleSessionExpiry(reason: ExpiryReason): Promise<void> {
switch (reason) {
case 'access_token_expired':
await this.handleAccessTokenExpiry();
break;
case 'refresh_token_expired':
await this.handleRefreshTokenExpiry();
break;
case 'idle_timeout':
await this.handleIdleTimeout();
break;
case 'absolute_timeout':
await this.handleAbsoluteTimeout();
break;
case 'security_violation':
await this.handleSecurityViolation();
break;
}
}
private async handleAccessTokenExpiry(): Promise<void> {
// Bu normal - sadece token'ı yenile
try {
await this.tokenManager.refreshAccessToken();
// Normal devam et, kullanıcı fark etmez
} catch (error) {
// Yenileme başarısız, tam session süresi dolması olarak ele al
await this.handleRefreshTokenExpiry();
}
}
private async handleRefreshTokenExpiry(): Promise<void> {
// Bu kullanıcının tekrar giriş yapmasını gerektirir
this.showSessionExpiredDialog();
await this.performLogout('session_expired');
}
private async handleIdleTimeout(): Promise<void> {
// Kullanıcı çok uzun süre hareketsizdi - biyometrik yeniden doğrulama iste
this.updateState({ userState: UserState.REQUIRES_VERIFICATION });
const biometricResult = await this.biometrics.authenticate();
if (biometricResult.success) {
// Aktivite timer'ını sıfırla
this.updateLastActivity();
this.updateState({ userState: UserState.AUTHENTICATED });
} else {
// Biyometrik başarısız, tam giriş iste
await this.performLogout('idle_timeout');
}
}
private showSessionExpiredDialog(): void {
Alert.alert(
'Oturum Süresi Doldu',
'Güvenlik nedeniyle oturumunuzun süresi doldu. Lütfen tekrar giriş yapın.',
[
{
text: 'Giriş Yap',
onPress: () => this.navigation.navigate('Login')
}
],
{ cancelable: false }
);
}
}
Tam Kullanıcı Yolculuğu#
Tüm bu parçalar gerçek bir kullanıcı yolculuğunda nasıl bir araya gelir:
- Uygulama Başlatma: Kullanıcının geçerli saklanan token'ları olup olmadığını kontrol et
- Token Doğrulama: Token'ların süresi dolmamış olduğunu doğrula
- Arka Plan Yenileme: Süresi dolan token'ları otomatik olarak yenile
- Aktivite İzleme: Boşta kalma süresi için kullanıcı aktivitesini izle
- Ağ Değişiklikleri: Offline/online senaryolarını ele al
- Uygulama Arka Plana Gitme: Arka plana gittiğinde uygulamayı güvenli yap
- Uygulama Ön Plana Gelme: Gerekirse kullanıcı kimliğini yeniden doğrula
- Çıkış Senaryoları: Kullanıcı başlattığı veya zorlanmış çıkışı ele al
Şimdi temelleri anladığınıza göre, implementasyon detaylarına dalalım.
Auth0'ı React Native ile Kurulum: Doğru Yöntem#
Önce temelleri doğru atalım. Hatalarımdan ders aldıktan sonra Auth0'ı nasıl kurduğumu göstereyim:
// auth/AuthService.ts
import Auth0 from 'react-native-auth0';
import * as Keychain from 'react-native-keychain';
import { Alert } from 'react-native';
class AuthService {
private auth0: Auth0;
private accessToken: string | null = null;
private refreshToken: string | null = null;
private tokenExpiresAt: number | null = null;
constructor() {
this.auth0 = new Auth0({
domain: 'your-tenant.auth0.com',
clientId: 'your-client-id',
// Kritik: Doğru scope'ları kullanın
scope: 'openid profile email offline_access'
});
}
async login(): Promise<void> {
try {
const credentials = await this.auth0.webAuth.authorize({
scope: 'openid profile email offline_access',
// Refresh token'lar için bu kritik
audience: 'https://your-api.com',
prompt: 'login',
// Daha iyi UX için özel parametreler
parameters: {
device: 'mobile',
login_hint: await this.getLastUsedEmail()
}
});
await this.handleAuthResponse(credentials);
} catch (error) {
console.error('Giriş başarısız:', error);
throw error;
}
}
private async handleAuthResponse(credentials: any): Promise<void> {
this.accessToken = credentials.accessToken;
this.refreshToken = credentials.refreshToken;
// Token süresini hesapla
const expiresIn = credentials.expiresIn || 3600; // Varsayılan 1 saat
this.tokenExpiresAt = Date.now() + (expiresIn * 1000);
// Token'ları güvenli şekilde sakla
await this.securelyStoreTokens();
}
}
Biyometrik Kimlik Doğrulama Dansı#
İşler burada ilginçleşiyor. Face ID/Touch ID implementasyonu sadece bir API çağırmak değil - ne zaman ve nasıl kullanacağınızı anlamakla ilgili. İşte savaşta test edilmiş yaklaşımım:
// auth/BiometricAuth.ts
import TouchID from 'react-native-touch-id';
import * as Keychain from 'react-native-keychain';
import { Platform } from 'react-native';
interface BiometricAuthResult {
success: boolean;
tokens?: { access: string; refresh: string };
error?: string;
}
class BiometricAuth {
private static readonly KEYCHAIN_SERVICE = 'com.yourapp.oauth';
private static readonly BIOMETRIC_THRESHOLD = 5 * 60 * 1000; // 5 dakika
static async authenticateWithBiometrics(): Promise<BiometricAuthResult> {
try {
// Biyometrik özelliklerin mevcut olup olmadığını kontrol et
const biometryType = await TouchID.isSupported();
if (!biometryType) {
return { success: false, error: 'Biyometrik özellik mevcut değil' };
}
// Biyometrik prompt'u yapılandır
const optionalConfigObject = {
title: 'Kimlik Doğrulama Gerekli',
imageColor: '#e00606',
imageErrorColor: '#ff0000',
sensorDescription: 'Parmak izi sensörü',
sensorErrorDescription: 'Başarısız',
cancelText: 'İptal',
fallbackLabel: 'Şifre Kullan',
unifiedErrors: false,
passcodeFallback: true
};
// Kimlik doğrula
await TouchID.authenticate(
'Hesabınıza erişmek için kimlik doğrulayın',
optionalConfigObject
);
// Saklanan token'ları al
const credentials = await Keychain.getInternetCredentials(
this.KEYCHAIN_SERVICE
);
if (!credentials) {
return { success: false, error: 'Saklanan kimlik bilgisi yok' };
}
// Saklanan token'ları parse et
const tokens = JSON.parse(credentials.password);
// Token'ların hala geçerli olup olmadığını kontrol et
if (await this.shouldRefreshTokens(tokens)) {
const newTokens = await AuthService.refreshTokens(tokens.refresh);
await this.storeTokens(newTokens);
return { success: true, tokens: newTokens };
}
return { success: true, tokens };
} catch (error) {
console.error('Biyometrik kimlik doğrulama başarısız:', error);
return { success: false, error: error.message };
}
}
private static async shouldRefreshTokens(tokens: any): Promise<boolean> {
// Token süresinin dolmasına yakın mıyız kontrol et
const expiresAt = tokens.expiresAt || 0;
const now = Date.now();
return (expiresAt - now) < this.BIOMETRIC_THRESHOLD;
}
}
Token Yaşam Döngüsü Yönetimi: Session Yönetiminin Kalbi#
Çoğu implementasyon burada başarısız oluyor. Mobil ortamda token yaşam döngülerini nasıl düzgün yöneteceğinizi göstereyim:
// auth/TokenManager.ts
import NetInfo from '@react-native-community/netinfo';
import BackgroundTimer from 'react-native-background-timer';
class TokenManager {
private refreshTimer: number | null = null;
private readonly TOKEN_REFRESH_MARGIN = 5 * 60 * 1000; // 5 dakika
private readonly MAX_RETRY_ATTEMPTS = 3;
async initializeTokenRefresh(): Promise<void> {
// Mevcut timer'ı temizle
this.stopTokenRefresh();
// Mevcut token süresini al
const expiresAt = await this.getTokenExpiration();
if (!expiresAt) return;
// Ne zaman yenileyeceğini hesapla (süre dolmadan 5 dakika önce)
const refreshTime = expiresAt - Date.now() - this.TOKEN_REFRESH_MARGIN;
if (refreshTime > 0) {
// Güvenilirlik için background timer kullan
this.refreshTimer = BackgroundTimer.setTimeout(async () => {
await this.performTokenRefresh();
}, refreshTime);
} else {
// Token hemen yenilenmeli
await this.performTokenRefresh();
}
}
private async performTokenRefresh(): Promise<void> {
let attempts = 0;
while (attempts < this.MAX_RETRY_ATTEMPTS) {
try {
// Önce ağ bağlantısını kontrol et
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected) {
// Ağ mevcut olduğunda tekrar denemeyi planla
const unsubscribe = NetInfo.addEventListener(state => {
if (state.isConnected) {
unsubscribe();
this.performTokenRefresh();
}
});
return;
}
// Saklanan refresh token'ı al
const refreshToken = await this.getRefreshToken();
if (!refreshToken) {
// Refresh token yok, kullanıcının tekrar giriş yapması gerekiyor
await this.handleSessionExpired();
return;
}
// Yenilemeyi gerçekleştir
const response = await auth0.oauth.refreshToken({
refreshToken,
scope: 'openid profile email offline_access'
});
// Yeni token'ları sakla
await this.updateTokens({
accessToken: response.accessToken,
refreshToken: response.refreshToken || refreshToken,
expiresIn: response.expiresIn
});
// Sonraki yenilemeyi planla
await this.initializeTokenRefresh();
break; // Başarılı, döngüden çık
} catch (error) {
attempts++;
if (error.error === 'invalid_grant') {
// Refresh token geçersiz/süresi dolmuş
await this.handleSessionExpired();
return;
}
if (attempts >= this.MAX_RETRY_ATTEMPTS) {
console.error('Maksimum denemeden sonra token yenileme başarısız:', error);
await this.handleSessionExpired();
return;
}
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, attempts) * 1000)
);
}
}
}
private async handleSessionExpired(): Promise<void> {
// Tüm saklanan token'ları temizle
await this.clearTokens();
// Kullanıcıyı bilgilendir
Alert.alert(
'Oturum Süresi Doldu',
'Oturumunuzun süresi doldu. Lütfen tekrar giriş yapın.',
[
{
text: 'Giriş Yap',
onPress: () => NavigationService.navigate('Login')
}
]
);
}
}
Arka Plan İşlemleri ve Push Bildirimleri#
İşte öğrendiğim kritik bir ders: iOS ve Android arka plan işlemlerini çok farklı şekilde ele alıyor ve session yönetiminizin bunu hesaba katması gerekiyor:
// services/BackgroundService.ts
import BackgroundFetch from 'react-native-background-fetch';
import PushNotification from 'react-native-push-notification';
class BackgroundService {
static async configure(): Promise<void> {
// Background fetch yapılandır
BackgroundFetch.configure({
minimumFetchInterval: 15, // dakika
forceAlarmManager: false,
stopOnTerminate: false,
startOnBoot: true,
enableHeadless: true,
requiresBatteryNotLow: false,
requiresCharging: false,
requiresStorageNotLow: false,
requiresDeviceIdle: false,
requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY
}, async (taskId) => {
// Bu arka planda çalışır
await this.performBackgroundTask();
BackgroundFetch.finish(taskId);
}, (error) => {
console.error('Background fetch başarısız:', error);
});
// Push bildirimlerini yapılandır
PushNotification.configure({
onRegister: async (token) => {
// FCM/APNS token'ı sakla
await this.registerDeviceToken(token.token);
},
onNotification: async (notification) => {
// Uygulama önde/arkadayken bildirimi işle
const isAuthenticated = await this.checkAuthStatus();
if (!isAuthenticated && notification.data.requiresAuth) {
// Bildirimi sonrası için sakla
await this.queueNotification(notification);
return;
}
// Bildirimi işle
await this.handleNotification(notification);
},
permissions: {
alert: true,
badge: true,
sound: true
},
popInitialNotification: true,
requestPermissions: true
});
}
private static async performBackgroundTask(): Promise<void> {
try {
// Geçerli token'larımız var mı kontrol et
const tokens = await TokenManager.getTokens();
if (!tokens || !tokens.accessToken) {
console.log('Arka plan görevi için geçerli oturum yok');
return;
}
// Token'ın yenilenmesi gerekiyor mu kontrol et
if (await TokenManager.shouldRefreshToken()) {
await TokenManager.performTokenRefresh();
}
// Arka plan işlemlerinizi gerçekleştirin
await this.syncDataInBackground();
} catch (error) {
console.error('Arka plan görevi hatası:', error);
}
}
private static async checkAuthStatus(): Promise<boolean> {
const tokens = await TokenManager.getTokens();
if (!tokens || !tokens.accessToken) return false;
// Token'ın süresi dolmuş mu kontrol et
const expiresAt = tokens.expiresAt || 0;
return Date.now() < expiresAt;
}
}
Optimal Token Süreleri: Öğrendiklerim#
Production'da çeşitli yapılandırmalarla deney yaptıktan sonra önerilerim:
// config/AuthConfig.ts
export const AuthConfig = {
// Access token: 15 dakika
// Neden: Güvenlik ile kullanıcı deneyimini dengeler
// Daha kısa = daha güvenli ama daha fazla refresh çağrısı
// Daha uzun = daha az güvenli ama daha iyi performans
accessTokenExpiration: 15 * 60, // saniye
// Refresh token: Mobil için 30 gün
// Neden: Mobil uygulamaların web'den farklı kullanım alışkanlıkları var
// Kullanıcılar uygulama açılışları arasında giriş yapmış kalmayı bekler
refreshTokenExpiration: 30 * 24 * 60 * 60, // saniye
// Boşta kalma süresi: 30 dakika
// Bu süre hareketsizlikten sonra biyometrik yeniden doğrulama iste
idleTimeout: 30 * 60 * 1000, // milisaniye
// Mutlak zaman aşımı: 7 gün
// Aktiviteden bağımsız olarak yeniden kimlik doğrulamayı zorla
absoluteTimeout: 7 * 24 * 60 * 60 * 1000, // milisaniye
// Arka plan yenileme eşiği: 5 dakika
// Süre dolmadan bu kadar dakika önce yenileme işlemini başlat
refreshThreshold: 5 * 60 * 1000, // milisaniye
};
Edge Case'leri Ele Almak: Kimsenin Bahsetmediği Şeyler#
Uykusuz gecelerime mal olan bazı edge case'leri paylaşayım:
1. Uygulama Sonlandırma ve Token Kalıcılığı#
// hooks/useAppState.ts
import { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
export const useAppState = () => {
const appState = useRef(AppState.currentState);
const lastActiveTime = useRef(Date.now());
useEffect(() => {
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
// Uygulama ön plana geldi
const inactiveTime = Date.now() - lastActiveTime.current;
if (inactiveTime > AuthConfig.idleTimeout) {
// Biyometrik yeniden kimlik doğrulama iste
const result = await BiometricAuth.authenticateWithBiometrics();
if (!result.success) {
// Giriş sayfasına yönlendir
NavigationService.navigate('Login');
}
} else {
// Sadece gerekirse token'ları yenile
await TokenManager.initializeTokenRefresh();
}
} else if (nextAppState.match(/inactive|background/)) {
// Uygulama arka plana gitti
lastActiveTime.current = Date.now();
}
appState.current = nextAppState;
};
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => subscription.remove();
}, []);
};
2. Ağ Bağlantısı ve Token Yenileme#
// services/NetworkAwareTokenManager.ts
class NetworkAwareTokenManager extends TokenManager {
private pendingRefresh: Promise<void> | null = null;
async performTokenRefresh(): Promise<void> {
// Aynı anda birden fazla yenileme girişimini önle
if (this.pendingRefresh) {
return this.pendingRefresh;
}
this.pendingRefresh = this.doRefresh();
try {
await this.pendingRefresh;
} finally {
this.pendingRefresh = null;
}
}
private async doRefresh(): Promise<void> {
const netInfo = await NetInfo.fetch();
if (!netInfo.isConnected) {
// Ağ yok, bağlantı bekle
return new Promise((resolve, reject) => {
const unsubscribe = NetInfo.addEventListener(state => {
if (state.isConnected) {
unsubscribe();
super.performTokenRefresh().then(resolve).catch(reject);
}
});
// 5 dakika sonra timeout
setTimeout(() => {
unsubscribe();
reject(new Error('Ağ zaman aşımı'));
}, 5 * 60 * 1000);
});
}
return super.performTokenRefresh();
}
}
3. Gerçekten Çalışan Logout Implementasyonu#
// auth/LogoutManager.ts
class LogoutManager {
static async logout(reason?: 'user_initiated' | 'session_expired' | 'security'): Promise<void> {
try {
// 1. Token'ları bellekten temizle
TokenManager.clearMemoryTokens();
// 2. Bekleyen refresh timer'larını iptal et
TokenManager.stopTokenRefresh();
// 3. Güvenli depolamayı temizle
await Keychain.resetInternetCredentials(AuthService.KEYCHAIN_SERVICE);
// 4. Cache'lenmiş kullanıcı verilerini temizle
await AsyncStorage.multiRemove([
'@user_profile',
'@user_preferences',
'@last_sync_time'
]);
// 5. Push bildirimlerini kaldır
await PushNotification.abandonPermissions();
// 6. Web cookie'lerini temizle (Auth0 için önemli)
if (Platform.OS === 'ios') {
await CookieManager.clearAll(true);
} else {
await CookieManager.clearAll();
}
// 7. Auth0'ı bilgilendir (opsiyonel ama tavsiye edilir)
if (reason === 'user_initiated') {
try {
await auth0.webAuth.clearSession();
} catch (error) {
// Hataları yoksay, kullanıcı zaten yerel olarak çıkış yaptı
console.log('Auth0 logout hatası:', error);
}
}
// 8. Navigasyonu sıfırla
NavigationService.reset('Auth');
// 9. Analytics event'i logla
Analytics.track('user_logged_out', { reason });
} catch (error) {
console.error('Logout hatası:', error);
// Bir şeyler başarısız olsa bile, kullanıcının korumalı içeriğe erişememesini sağla
NavigationService.reset('Auth');
}
}
}
Zor Yoldan Öğrendiğim Güvenlik En İyi Uygulamaları#
- Hassas verileri asla AsyncStorage'da saklama - şifrelenmemiş
- Token'lar için her zaman Keychain/Keystore kullan
- Auth0 endpoint'leri için certificate pinning uygula
- Yüksek güvenlikli uygulamalar için jailbreak/root tespiti ekle
- Biyometrik kimlik doğrulamayı bir kapı olarak kullan, birincil kimlik doğrulama olarak değil
// security/SecurityManager.ts
import JailMonkey from 'jail-monkey';
import { Platform } from 'react-native';
class SecurityManager {
static async checkDeviceSecurity(): Promise<{ secure: boolean; reason?: string }> {
// Jailbreak/root kontrolü
if (JailMonkey.isJailBroken()) {
return { secure: false, reason: 'cihaz_güvenliği_ihlal_edilmiş' };
}
// Debugger kontrolü
if (JailMonkey.isDebuggedMode()) {
return { secure: false, reason: 'debugger_bağlı' };
}
// Uygulama bütünlüğü kontrolü (sadece iOS)
if (Platform.OS === 'ios' && !JailMonkey.isOnExternalStorage()) {
return { secure: false, reason: 'uygulama_değiştirilmiş' };
}
return { secure: true };
}
static async initializeSecurity(): Promise<void> {
const { secure, reason } = await this.checkDeviceSecurity();
if (!secure) {
Alert.alert(
'Güvenlik Uyarısı',
'Cihazınız güvenlik açığına sahip görünüyor. Bazı özellikler devre dışı bırakılabilir.',
[{ text: 'Tamam' }]
);
// Güvenliği ihlal edilmiş cihazlar için işlevselliği kısıtla
await this.enableRestrictedMode();
}
}
}
Toparlama: Gerçekten Önemli Olanlar#
Tüm bu session yönetimi savaşlarından sonra öğrendiklerim:
- Gerçek cihazlarda test et - Simülatörler gerçek davranışı göstermiyor
- Offline senaryolar için plan yap - Mobil uygulamalar her zaman bağlı değil
- Platform farklılıklarına saygı duy - iOS ve Android arka plan görevlerini farklı ele alıyor
- Token yenileme hatalarını izle - Erken uyarı sisteminiz onlar
- Refresh token'ları güvende tut - Esasen kalıcı şifreler
En büyük ders? Mobil uygulamalarda session yönetimi web uygulamalarından temelden farklı. Kısıtlamalar farklı, kullanıcı beklentileri farklı ve güvenlik modeli farklı.
Bir dahaki sefere React Native uygulamasında Auth0 uygularken unutmayın: sadece giriş işlemini çalıştırmak değil mesele. Ağ bağlantısı kopuk olduğunda, uygulama saatlerce arka planda kaldığında ve kullanıcı sadece uygulamayı açıp çalışmasını beklediğinde çalışan kesintisiz, güvenli bir deneyim yaratmak.
Sorularınız veya kendi savaş hikayeleriniz var mı? Duymayı çok isterim. Bunlar mobil geliştirmeyi hem zorlu hem de ödüllendirici yapan türden problemler.
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!