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:

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

  1. 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
  2. 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ı
  3. 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:

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

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

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

  1. Uygulama Başlatma: Kullanıcının geçerli saklanan token'ları olup olmadığını kontrol et
  2. Token Doğrulama: Token'ların süresi dolmamış olduğunu doğrula
  3. Arka Plan Yenileme: Süresi dolan token'ları otomatik olarak yenile
  4. Aktivite İzleme: Boşta kalma süresi için kullanıcı aktivitesini izle
  5. Ağ Değişiklikleri: Offline/online senaryolarını ele al
  6. Uygulama Arka Plana Gitme: Arka plana gittiğinde uygulamayı güvenli yap
  7. Uygulama Ön Plana Gelme: Gerekirse kullanıcı kimliğini yeniden doğrula
  8. Çı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:

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

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

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

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

TypeScript
// 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ığı#

TypeScript
// 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#

TypeScript
// 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#

TypeScript
// 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ı#

  1. Hassas verileri asla AsyncStorage'da saklama - şifrelenmemiş
  2. Token'lar için her zaman Keychain/Keystore kullan
  3. Auth0 endpoint'leri için certificate pinning uygula
  4. Yüksek güvenlikli uygulamalar için jailbreak/root tespiti ekle
  5. Biyometrik kimlik doğrulamayı bir kapı olarak kullan, birincil kimlik doğrulama olarak değil
TypeScript
// 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:

  1. Gerçek cihazlarda test et - Simülatörler gerçek davranışı göstermiyor
  2. Offline senaryolar için plan yap - Mobil uygulamalar her zaman bağlı değil
  3. Platform farklılıklarına saygı duy - iOS ve Android arka plan görevlerini farklı ele alıyor
  4. Token yenileme hatalarını izle - Erken uyarı sisteminiz onlar
  5. 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.

Loading...

Yorumlar (0)

Sohbete katıl

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

Henüz yorum yok

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

Related Posts