İçeriğe atla

React Native'de Auth0 Oturum Yönetimi ve Biyometrik Giriş

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

Ayhan Sipahi Ayhan Sipahi

Mobil uygulamalarda oturum yönetimi, web geliştiricilerinin nadiren karşılaştığı zorluklar çıkarır. Bir mobil uygulama; arka plan durumlarını, ağ kesintilerini, biyometrik kimlik doğrulamayı ve platforma özgü güvenlik kısıtlamalarını, kullanıcı deneyimini bozmadan yönetmek zorundadır. React Native tarafında ayakta kalan varsayılan şu: refresh token rotasyonunu Auth0 tenant’ına bırakın, token’ları Keychain veya Keystore içinde biyometrik kapının arkasında tutun ve 401’e tepki vermek yerine süre dolmadan yenileyin.

Parçalar şunlar: zamanlayıcıyla ve ağ geri geldiğinde çalışan bir token yöneticisi, platformun güvenli deposunun önünde duran bir biyometrik kapı, boşta kalma zaman aşımını uygulayan bir app-state hook’u ve bunları bir araya getiren bir auth context. Her adım bir öncekinin üzerine biner; kod sırayla okunmak üzere yazıldı.

Ön Gereksinimler#

Başlamadan önce şunlara sahip olduğunuzdan emin olun:

  • React Native geliştirme ortamı yapılandırılmış (React Native 0.78+)
  • Yapılandırılmış uygulamaya sahip Auth0 hesabı
  • iOS ve Android geliştirme ortamları kurulu
  • OAuth 2.0 ve JWT token’larının temel anlayışı
  • React Native navigasyon ve state yönetimine aşinalık

Gerekli paketler:

{
  "react-native": "^0.78.0",
  "react-native-auth0": "^4.6.0",
  "react-native-biometrics": "^3.0.1",
  "react-native-keychain": "^10.0.0",
  "@react-native-async-storage/async-storage": "^1.21.0",
  "@react-native-community/netinfo": "^11.2.0",
  "react-native-background-fetch": "^4.2.0"
}

Uyumluluk notları:

  • React Native New Architecture: Paketlerin tamamı New Architecture (Fabric/TurboModules) ile uyumludur
  • Expo: Expo dev build’leriyle uyumludur (native bağımlılıklar için custom development client gerekir)
  • iOS arka plan kısıtı: Arka plan fetch’i sistemin belirlediği aralıklarda çalışır; token yenileme için güvenilir bir yol değildir

Adım 1: Oturum Yönetimi Mimarisini Anlamak#

Devamının tamamı iki şeye dayanır: oturumun geçtiği durumlar ve her token’a atanan ömür.

Oturum Durumları ve Yaşam Döngüsü#

Mobil uygulamalar, çeşitli kimlik doğrulama senaryolarını yönetmek için ayrıntılı bir durum modeli gerektirir:

// types/AuthState.ts
export enum UserState {
  UNAUTHENTICATED = 'unauthenticated',
  AUTHENTICATING = 'authenticating',
  AUTHENTICATED = 'authenticated',
  SESSION_EXPIRED = 'session_expired',
  REQUIRES_VERIFICATION = 'requires_verification',
  CHECKING_AUTH = 'checking_auth',
  LOGGING_OUT = 'logging_out'
}

export interface AuthState {
  userState: UserState;
  user: User | null;
  tokens: {
    accessToken: string | null;
    refreshToken: string | null;
    idToken: string | null;
    expiresAt: number | null;
  };
  lastActivity: number;
  biometricEnabled: boolean;
  sessionStartTime: number;
}

export interface User {
  id: string;
  email: string;
  name: string;
  picture?: string;
  emailVerified: boolean;
}

Token Mimarisi#

Ömürleri farklı üç token var ve üçü de uygulama kodunda değil, Auth0 tenant’ında yapılandırılır. Aşağıdaki değerler bir mobil istemci için işe yarar bir başlangıç noktasıdır:

  1. Access Token (15 dakika): API yetkilendirmesi için kullanılır
  2. Refresh Token (30 gün mutlak ömür): Yeni access token’ları almak için kullanılır
  3. ID Token (1 saat): Kullanıcı profil claim’lerini taşır

Adım 2: Auth0 Entegrasyonunu Kurmak#

// services/auth/AuthService.ts
import Auth0 from 'react-native-auth0';
import Config from 'react-native-config';

class AuthService {
  private auth0: Auth0;
  private readonly AUTH0_SCOPE = 'openid profile email offline_access';

  constructor() {
    this.auth0 = new Auth0({
      domain: Config.AUTH0_DOMAIN,
      clientId: Config.AUTH0_CLIENT_ID,
    });
  }

  async login(): Promise<Credentials> {
    try {
      const credentials = await this.auth0.webAuth.authorize({
        scope: this.AUTH0_SCOPE,
        audience: Config.AUTH0_AUDIENCE,
        prompt: 'login',
        // Mobil için ek parametreler
        parameters: {
          device: 'mobile'
        }
      });

      return this.processCredentials(credentials);
    } catch (error) {
      if (error.error === 'a0.session.user_cancelled') {
        throw new Error('Kullanıcı girişi iptal etti');
      }
      throw error;
    }
  }

  async refreshTokens(refreshToken: string): Promise<Credentials> {
    try {
      // Rotasyon tenant ayarıdır; SDK yalnızca refresh token'ı takas eder
      const credentials = await this.auth0.auth.refreshToken({
        refreshToken,
        scope: this.AUTH0_SCOPE
      });

      return this.processCredentials(credentials);
    } catch (error) {
      if (error.error === 'invalid_grant') {
        throw new Error('Refresh token süresi dolmuş veya iptal edilmiş');
      }
      throw error;
    }
  }

  async logout(): Promise<void> {
    // Auth0 oturumunu ve çerezleri temizle
    await this.auth0.webAuth.clearSession();
  }

  async getUserInfo(accessToken: string): Promise<User> {
    const userInfo = await this.auth0.auth.userInfo({ token: accessToken });

    return {
      id: userInfo.sub,
      email: userInfo.email,
      name: userInfo.name || userInfo.nickname,
      picture: userInfo.picture,
      emailVerified: userInfo.email_verified
    };
  }

  private processCredentials(credentials: any): Credentials {
    return {
      accessToken: credentials.accessToken,
      refreshToken: credentials.refreshToken,
      idToken: credentials.idToken,
      expiresIn: credentials.expiresIn,
      expiresAt: Date.now() + (credentials.expiresIn * 1000),
      tokenType: credentials.tokenType,
      scope: credentials.scope
    };
  }
}

export default new AuthService();

Adım 3: Güvenli Token Depolamayı Uygulamak#

Keychain ve Keystore token baytlarını saklar; AsyncStorage yalnızca kaybı sorun olmayan metadata’yı görür.

// services/storage/SecureStorage.ts
import * as Keychain from 'react-native-keychain';
import AsyncStorage from '@react-native-async-storage/async-storage';
import CryptoJS from 'crypto-js';

class SecureStorage {
  private readonly KEYCHAIN_SERVICE = 'com.yourapp.oauth';
  private readonly KEYCHAIN_ACCESS_GROUP = 'group.com.yourapp';

  async storeTokens(tokens: TokenSet): Promise<void> {
    try {
      // Hassas token'ları Keychain'de sakla
      await Keychain.setInternetCredentials(
        this.KEYCHAIN_SERVICE,
        'tokens',
        JSON.stringify({
          accessToken: tokens.accessToken,
          refreshToken: tokens.refreshToken,
          expiresAt: tokens.expiresAt
        }),
        {
          accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
          accessControl: Keychain.ACCESS_CONTROL.BIOMETRY_CURRENT_SET,
          accessGroup: this.KEYCHAIN_ACCESS_GROUP,
          authenticationPrompt: 'Hesabınıza erişmek için kimlik doğrulayın'
        }
      );

      // Hassas olmayan verileri AsyncStorage'da sakla
      await AsyncStorage.setItem('@auth_metadata', JSON.stringify({
        expiresAt: tokens.expiresAt,
        scope: tokens.scope,
        tokenType: tokens.tokenType
      }));
    } catch (error) {
      console.error('Tokenlar güvenli şekilde saklanamadı:', error);
      throw new Error('Güvenli depolama başarısız');
    }
  }

  async getTokens(): Promise<TokenSet | null> {
    try {
      const credentials = await Keychain.getInternetCredentials(
        this.KEYCHAIN_SERVICE
      );

      if (!credentials) {
        return null;
      }

      const tokens = JSON.parse(credentials.password);
      const metadata = await AsyncStorage.getItem('@auth_metadata');

      return {
        ...tokens,
        ...(metadata ? JSON.parse(metadata) : {})
      };
    } catch (error) {
      console.error('Tokenlar alınamadı:', error);
      return null;
    }
  }

  async clearTokens(): Promise<void> {
    await Keychain.resetInternetCredentials(this.KEYCHAIN_SERVICE);
    await AsyncStorage.removeItem('@auth_metadata');
  }

  async hasStoredCredentials(): Promise<boolean> {
    try {
      const credentials = await Keychain.hasInternetCredentials(
        this.KEYCHAIN_SERVICE
      );
      return credentials;
    } catch {
      return false;
    }
  }
}

export default new SecureStorage();

Adım 4: Biyometrik Kimlik Doğrulamayı Uygulamak#

react-native-biometrics, yukarıdaki depolama katmanının önünde duran kapıdır.

// services/auth/BiometricService.ts
import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';
import AsyncStorage from '@react-native-async-storage/async-storage';
import SecureStorage from '../storage/SecureStorage';
import AuthService from './AuthService';

class BiometricService {
  private rnBiometrics: ReactNativeBiometrics;

  constructor() {
    this.rnBiometrics = new ReactNativeBiometrics({
      allowDeviceCredentials: true
    });
  }

  async isBiometricAvailable(): Promise<{
    available: boolean;
    biometryType: BiometryTypes | null;
  }> {
    try {
      const { available, biometryType } = await this.rnBiometrics.isSensorAvailable();
      return { available, biometryType };
    } catch (error) {
      console.error('Biyometrik kontrolü başarısız:', error);
      return { available: false, biometryType: null };
    }
  }

  async authenticateWithBiometrics(): Promise<AuthenticationResult> {
    try {
      const { available, biometryType } = await this.isBiometricAvailable();

      if (!available) {
        return {
          success: false,
          error: 'Biyometrik kimlik doğrulama mevcut değil'
        };
      }

      // Kimlik doğrulama için imza oluştur
      const { success, signature } = await this.rnBiometrics.createSignature({
        promptMessage: 'Hesabınıza erişmek için kimlik doğrulayın',
        payload: Date.now().toString(),
        cancelButtonText: 'İptal'
      });

      if (!success) {
        return {
          success: false,
          error: 'Biyometrik kimlik doğrulama başarısız'
        };
      }

      // Başarılı kimlik doğrulamadan sonra saklanan token'ları al
      const tokens = await SecureStorage.getTokens();

      if (!tokens) {
        return {
          success: false,
          error: 'Saklanan kimlik bilgileri bulunamadı'
        };
      }

      // Token'ların yenilenmesi gerekip gerekmediğini kontrol et
      if (this.shouldRefreshTokens(tokens)) {
        const refreshedTokens = await AuthService.refreshTokens(tokens.refreshToken);
        await SecureStorage.storeTokens(refreshedTokens);
        return { success: true, tokens: refreshedTokens };
      }

      return { success: true, tokens };
    } catch (error) {
      console.error('Biyometrik kimlik doğrulama hatası:', error);
      return {
        success: false,
        error: error.message || 'Bilinmeyen hata oluştu'
      };
    }
  }

  async enrollBiometrics(): Promise<boolean> {
    try {
      const { available } = await this.isBiometricAvailable();

      if (!available) {
        return false;
      }

      // Biyometrik kayıt için anahtar çifti oluştur
      const { publicKey } = await this.rnBiometrics.createKeys();

      // Gelecekteki doğrulama için genel anahtarı sakla
      await AsyncStorage.setItem('@biometric_public_key', publicKey);

      return true;
    } catch (error) {
      console.error('Biyometrik kayıt başarısız:', error);
      return false;
    }
  }

  async deleteBiometricKeys(): Promise<void> {
    await this.rnBiometrics.deleteKeys();
    await AsyncStorage.removeItem('@biometric_public_key');
  }

  private shouldRefreshTokens(tokens: TokenSet): boolean {
    const REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 dakika
    const timeUntilExpiry = tokens.expiresAt - Date.now();
    return timeUntilExpiry < REFRESH_THRESHOLD;
  }
}

export default new BiometricService();

Adım 5: Token Yöneticisini Oluşturmak#

// services/auth/TokenManager.ts
import NetInfo from '@react-native-community/netinfo';
import BackgroundFetch from 'react-native-background-fetch';
import AuthService from './AuthService';
import SecureStorage from '../storage/SecureStorage';

class TokenManager {
  private refreshTimer: NodeJS.Timeout | null = null;
  private refreshPromise: Promise<void> | null = null;
  private readonly REFRESH_THRESHOLD = 5 * 60 * 1000; // 5 dakika
  private readonly MAX_RETRY_ATTEMPTS = 3;
  private readonly RETRY_DELAY_BASE = 1000; // 1 saniye

  async initialize(): Promise<void> {
    // Otomatik token yenilemeyi ayarla
    await this.scheduleTokenRefresh();

    // Token yenileme için arka plan fetch'i yapılandır
    await this.configureBackgroundRefresh();

    // Ağ değişikliklerini dinle
    NetInfo.addEventListener(this.handleNetworkChange);
  }

  async scheduleTokenRefresh(): Promise<void> {
    // Mevcut zamanlayıcıyı temizle
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = null;
    }

    const tokens = await SecureStorage.getTokens();
    if (!tokens || !tokens.expiresAt) {
      return;
    }

    const timeUntilExpiry = tokens.expiresAt - Date.now();
    const refreshTime = Math.max(0, timeUntilExpiry - this.REFRESH_THRESHOLD);

    if (refreshTime > 0) {
      this.refreshTimer = setTimeout(() => {
        this.performTokenRefresh();
      }, refreshTime);
    } else {
      // Token'ın hemen yenilenmesi gerekiyor
      await this.performTokenRefresh();
    }
  }

  async performTokenRefresh(): Promise<void> {
    // Eş zamanlı yenileme girişimlerini önle
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = this.doRefreshWithRetry();

    try {
      await this.refreshPromise;
    } finally {
      this.refreshPromise = null;
    }
  }

  private async doRefreshWithRetry(): Promise<void> {
    let lastError: Error | null = null;

    for (let attempt = 0; attempt < this.MAX_RETRY_ATTEMPTS; attempt++) {
      try {
        // Ağ bağlantısını kontrol et
        const netInfo = await NetInfo.fetch();
        if (!netInfo.isConnected) {
          throw new Error('Ağ bağlantısı yok');
        }

        // Mevcut token'ları al
        const tokens = await SecureStorage.getTokens();
        if (!tokens || !tokens.refreshToken) {
          throw new Error('Refresh token mevcut değil');
        }

        // Yenilemeyi gerçekleştir
        const newTokens = await AuthService.refreshTokens(tokens.refreshToken);

        // Yeni token'ları sakla
        await SecureStorage.storeTokens(newTokens);

        // Sonraki yenilemeyi planla
        await this.scheduleTokenRefresh();

        // Başarı olayını yayınla
        this.emitTokenRefreshSuccess();

        return;
      } catch (error) {
        lastError = error;

        if (error.message === 'Refresh token süresi dolmuş veya iptal edilmiş') {
          // Bundan kurtulunamaz - kullanıcının tekrar giriş yapması gerekiyor
          this.handleSessionExpired();
          return;
        }

        if (attempt < this.MAX_RETRY_ATTEMPTS - 1) {
          // Üstel geri çekilme gecikmesini hesapla
          const delay = this.RETRY_DELAY_BASE * Math.pow(2, attempt);
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }

    // Tüm yeniden denemeler başarısız oldu
    console.error('Token yenileme tüm denemelerden sonra başarısız:', lastError);
    this.handleSessionExpired();
  }

  private async configureBackgroundRefresh(): Promise<void> {
    await BackgroundFetch.configure({
      minimumFetchInterval: 15, // 15 dakika
      forceAlarmManager: false,
      stopOnTerminate: false,
      enableHeadless: true,
      startOnBoot: true,
      requiredNetworkType: BackgroundFetch.NETWORK_TYPE_ANY
    }, async (taskId) => {
      try {
        await this.performTokenRefresh();
      } catch (error) {
        console.error('Arka plan token yenilemesi başarısız:', error);
      } finally {
        BackgroundFetch.finish(taskId);
      }
    }, (taskId) => {
      console.error('Arka plan fetch zaman aşımı');
      BackgroundFetch.finish(taskId);
    });

    // iOS'ta arka plan fetch'i ciddi biçimde kısıtlıdır ve güvenilir çalışmayabilir
    // Kritik token yenileme senaryoları için sessiz push bildirimlerini değerlendirin
  }

  private handleNetworkChange = async (state: any) => {
    if (state.isConnected && this.refreshPromise === null) {
      // Ağ yeniden bağlandı, token'ların yenilenmesi gerekip gerekmediğini kontrol et
      const tokens = await SecureStorage.getTokens();
      if (tokens && this.shouldRefreshTokens(tokens)) {
        await this.performTokenRefresh();
      }
    }
  };

  private shouldRefreshTokens(tokens: TokenSet): boolean {
    const timeUntilExpiry = tokens.expiresAt - Date.now();
    return timeUntilExpiry < this.REFRESH_THRESHOLD;
  }

  private handleSessionExpired(): void {
    // Token'ları temizle
    SecureStorage.clearTokens();

    // Oturum sona erdi olayını yayınla
    this.emitSessionExpired();
  }

  private emitTokenRefreshSuccess(): void {
    // Uygulamanın işlemesi için olay yayınla
    EventEmitter.emit('auth:token-refreshed');
  }

  private emitSessionExpired(): void {
    // Uygulamanın işlemesi için olay yayınla
    EventEmitter.emit('auth:session-expired');
  }

  cleanup(): void {
    if (this.refreshTimer) {
      clearTimeout(this.refreshTimer);
      this.refreshTimer = null;
    }
    BackgroundFetch.stop();
  }
}

export default new TokenManager();

Adım 6: Uygulama Durum Geçişlerini Yönetmek#

Boşta kalma zaman aşımı ve arka plandan dönüşteki biyometrik kontrol, ikisi de burada AppState geçişlerine bağlı olarak çalışır.

// hooks/useAppStateAuth.ts
import { useEffect, useRef } from 'react';
import { AppState, AppStateStatus } from 'react-native';
import BiometricService from '../services/auth/BiometricService';
import TokenManager from '../services/auth/TokenManager';

interface AppStateAuthConfig {
  idleTimeout?: number; // milisaniye
  requireBiometricOnForeground?: boolean;
  onSessionExpired?: () => void;
  onAuthRequired?: () => void;
}

export const useAppStateAuth = (config: AppStateAuthConfig = {}) => {
  const {
    idleTimeout = 30 * 60 * 1000, // varsayılan 30 dakika
    requireBiometricOnForeground = true,
    onSessionExpired,
    onAuthRequired
  } = config;

  const appState = useRef(AppState.currentState);
  const backgroundTime = useRef<number>(0);

  useEffect(() => {
    const handleAppStateChange = async (nextAppState: AppStateStatus) => {
      // Uygulama ön plana geliyor
      if (
        appState.current.match(/inactive|background/) &&
        nextAppState === 'active'
      ) {
        const timeInBackground = Date.now() - backgroundTime.current;

        if (timeInBackground > idleTimeout) {
          // Oturum zaman aşımına uğradı
          onSessionExpired?.();
        } else if (requireBiometricOnForeground && timeInBackground > 60000) {
          // Arka planda 1 dakikadan fazla kaldıysa biyometrik iste
          const result = await BiometricService.authenticateWithBiometrics();

          if (!result.success) {
            onAuthRequired?.();
          } else {
            // Gerekirse token'ları yenile
            await TokenManager.scheduleTokenRefresh();
          }
        } else {
          // Sadece token'ların güncel olduğundan emin ol
          await TokenManager.scheduleTokenRefresh();
        }
      }
      // Uygulama arka plana gidiyor
      else if (
        appState.current === 'active' &&
        nextAppState.match(/inactive|background/)
      ) {
        backgroundTime.current = Date.now();
      }

      appState.current = nextAppState;
    };

    const subscription = AppState.addEventListener('change', handleAppStateChange);

    return () => {
      subscription.remove();
    };
  }, [idleTimeout, requireBiometricOnForeground, onSessionExpired, onAuthRequired]);
};

Adım 7: Auth Context’i Uygulamak#

// contexts/AuthContext.tsx
import React, { createContext, useContext, useEffect, useReducer } from 'react';
import AsyncStorage from '@react-native-async-storage/async-storage';
import AuthService from '../services/auth/AuthService';
import BiometricService from '../services/auth/BiometricService';
import SecureStorage from '../services/storage/SecureStorage';
import TokenManager from '../services/auth/TokenManager';
import { useAppStateAuth } from '../hooks/useAppStateAuth';
import { AuthState, UserState } from '../types/AuthState';

interface AuthContextValue {
  state: AuthState;
  login: () => Promise<void>;
  logout: () => Promise<void>;
  authenticateWithBiometrics: () => Promise<boolean>;
  enableBiometrics: () => Promise<boolean>;
  checkAuthStatus: () => Promise<void>;
}

const AuthContext = createContext<AuthContextValue | null>(null);

export const useAuth = () => {
  const context = useContext(AuthContext);
  if (!context) {
    throw new Error('useAuth AuthProvider içinde kullanılmalıdır');
  }
  return context;
};

const authReducer = (state: AuthState, action: any): AuthState => {
  switch (action.type) {
    case 'SET_USER_STATE':
      return { ...state, userState: action.payload };
    case 'SET_AUTH_DATA':
      return {
        ...state,
        userState: UserState.AUTHENTICATED,
        user: action.payload.user,
        tokens: action.payload.tokens,
        sessionStartTime: Date.now(),
        lastActivity: Date.now()
      };
    case 'CLEAR_AUTH':
      return {
        ...state,
        userState: UserState.UNAUTHENTICATED,
        user: null,
        tokens: {
          accessToken: null,
          refreshToken: null,
          idToken: null,
          expiresAt: null
        },
        sessionStartTime: 0,
        lastActivity: 0
      };
    case 'SET_BIOMETRIC_ENABLED':
      return { ...state, biometricEnabled: action.payload };
    default:
      return state;
  }
};

export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [state, dispatch] = useReducer(authReducer, {
    userState: UserState.CHECKING_AUTH,
    user: null,
    tokens: {
      accessToken: null,
      refreshToken: null,
      idToken: null,
      expiresAt: null
    },
    lastActivity: 0,
    biometricEnabled: false,
    sessionStartTime: 0
  });

  // Uygulama durum değişikliklerini yönet
  useAppStateAuth({
    idleTimeout: 30 * 60 * 1000,
    requireBiometricOnForeground: state.biometricEnabled,
    onSessionExpired: () => {
      dispatch({ type: 'CLEAR_AUTH' });
    },
    onAuthRequired: () => {
      dispatch({ type: 'SET_USER_STATE', payload: UserState.REQUIRES_VERIFICATION });
    }
  });

  // İlk auth durumunu kontrol et
  useEffect(() => {
    checkAuthStatus();

    // Oturum sona erdi olaylarını dinle
    const handleSessionExpired = () => {
      dispatch({ type: 'CLEAR_AUTH' });
    };

    EventEmitter.addListener('auth:session-expired', handleSessionExpired);

    return () => {
      EventEmitter.removeListener('auth:session-expired', handleSessionExpired);
    };
  }, []);

  const checkAuthStatus = async () => {
    try {
      dispatch({ type: 'SET_USER_STATE', payload: UserState.CHECKING_AUTH });

      const tokens = await SecureStorage.getTokens();

      if (!tokens || !tokens.accessToken) {
        dispatch({ type: 'CLEAR_AUTH' });
        return;
      }

      // Token'ların süresi dolmuş mu kontrol et
      if (tokens.expiresAt && tokens.expiresAt < Date.now()) {
        // Yenilemeyi dene
        try {
          await TokenManager.performTokenRefresh();
          const newTokens = await SecureStorage.getTokens();

          if (newTokens) {
            const user = await AuthService.getUserInfo(newTokens.accessToken);
            dispatch({
              type: 'SET_AUTH_DATA',
              payload: { user, tokens: newTokens }
            });

            // Token yöneticisini başlat
            await TokenManager.initialize();
          }
        } catch {
          dispatch({ type: 'CLEAR_AUTH' });
        }
      } else {
        // Token'lar hala geçerli
        const user = await AuthService.getUserInfo(tokens.accessToken);
        dispatch({
          type: 'SET_AUTH_DATA',
          payload: { user, tokens }
        });

        // Token yöneticisini başlat
        await TokenManager.initialize();
      }

      // Biyometrik kaydı kontrol et
      const { available } = await BiometricService.isBiometricAvailable();
      if (available) {
        const enrolled = await AsyncStorage.getItem('@biometric_enrolled');
        dispatch({ type: 'SET_BIOMETRIC_ENABLED', payload: enrolled === 'true' });
      }
    } catch (error) {
      console.error('Auth durumu kontrolü başarısız:', error);
      dispatch({ type: 'CLEAR_AUTH' });
    }
  };

  const login = async () => {
    try {
      dispatch({ type: 'SET_USER_STATE', payload: UserState.AUTHENTICATING });

      const credentials = await AuthService.login();
      await SecureStorage.storeTokens(credentials);

      const user = await AuthService.getUserInfo(credentials.accessToken);

      dispatch({
        type: 'SET_AUTH_DATA',
        payload: { user, tokens: credentials }
      });

      // Token yöneticisini başlat
      await TokenManager.initialize();

      // Biyometrik kaydı öner
      const { available } = await BiometricService.isBiometricAvailable();
      if (available && !state.biometricEnabled) {
        // Kullanıcıya biyometrikleri etkinleştirmesini iste
        // Bu genellikle bir modal gösterir veya ayarlara yönlendirir
      }
    } catch (error) {
      dispatch({ type: 'CLEAR_AUTH' });
      throw error;
    }
  };

  const logout = async () => {
    try {
      dispatch({ type: 'SET_USER_STATE', payload: UserState.LOGGING_OUT });

      // Token yöneticisini temizle
      TokenManager.cleanup();

      // Auth0 oturumunu temizle
      await AuthService.logout();

      // Saklanan token'ları temizle
      await SecureStorage.clearTokens();

      // Etkinse biyometrik anahtarları temizle
      if (state.biometricEnabled) {
        await BiometricService.deleteBiometricKeys();
      }

      dispatch({ type: 'CLEAR_AUTH' });
    } catch (error) {
      console.error('Çıkış hatası:', error);
      // Bir şey başarısız olsa bile çıkışı zorla
      dispatch({ type: 'CLEAR_AUTH' });
    }
  };

  const authenticateWithBiometrics = async (): Promise<boolean> => {
    const result = await BiometricService.authenticateWithBiometrics();

    if (result.success && result.tokens) {
      const user = await AuthService.getUserInfo(result.tokens.accessToken);
      dispatch({
        type: 'SET_AUTH_DATA',
        payload: { user, tokens: result.tokens }
      });
      return true;
    }

    return false;
  };

  const enableBiometrics = async (): Promise<boolean> => {
    const enrolled = await BiometricService.enrollBiometrics();

    if (enrolled) {
      await AsyncStorage.setItem('@biometric_enrolled', 'true');
      dispatch({ type: 'SET_BIOMETRIC_ENABLED', payload: true });
      return true;
    }

    return false;
  };

  const value = {
    state,
    login,
    logout,
    authenticateWithBiometrics,
    enableBiometrics,
    checkAuthStatus
  };

  return (
    <AuthContext.Provider value={value}>
      {children}
    </AuthContext.Provider>
  );
};

Adım 8: Korumalı Route’lar Oluşturmak#

Kullanıcının gördüğü ekran tamamen userState’e bağlıdır.

// navigation/AuthNavigator.tsx
import React from 'react';
import { NavigationContainer } from '@react-navigation/native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { useAuth } from '../contexts/AuthContext';
import { UserState } from '../types/AuthState';

// Ekranları içe aktar
import LoginScreen from '../screens/LoginScreen';
import BiometricPromptScreen from '../screens/BiometricPromptScreen';
import HomeScreen from '../screens/HomeScreen';
import LoadingScreen from '../screens/LoadingScreen';

const Stack = createNativeStackNavigator();

export const AuthNavigator: React.FC = () => {
  const { state } = useAuth();

  if (state.userState === UserState.CHECKING_AUTH) {
    return <LoadingScreen />;
  }

  return (
    <NavigationContainer>
      <Stack.Navigator screenOptions={{ headerShown: false }}>
        {state.userState === UserState.AUTHENTICATED ? (
          <Stack.Group>
            <Stack.Screen name="Home" component={HomeScreen} />
            {/* Diğer kimlik doğrulamalı ekranları ekle */}
          </Stack.Group>
        ) : state.userState === UserState.REQUIRES_VERIFICATION ? (
          <Stack.Screen name="BiometricPrompt" component={BiometricPromptScreen} />
        ) : (
          <Stack.Screen name="Login" component={LoginScreen} />
        )}
      </Stack.Navigator>
    </NavigationContainer>
  );
};

Hata Modları ve Çözümleri#

Token Yenileme Hataları#

invalid_grant hatasıyla dönen bir yenileme çağrısı, neredeyse her zaman token’ın daha önce iptal edildiğini ya da süresinin dolduğunu gösterir; aşağıdaki kod isteğin sessizce başarısız olmasına izin vermek yerine bu durumu açıkça ele alır.

// TokenManager'da
if (error.error === 'invalid_grant') {
  // Yerel token'ları temizle
  await SecureStorage.clearTokens();

  // Yeniden kimlik doğrulamayı zorla
  EventEmitter.emit('auth:session-expired');
}

Biyometrik Kimlik Doğrulama Döngüsü#

Başarısız bir biyometrik kontrol, aynı istemi hemen yeniden tetiklememeli.

class BiometricService {
  private failureCount = 0;
  private readonly MAX_FAILURES = 3;

  async authenticateWithBackoff(): Promise<AuthenticationResult> {
    if (this.failureCount >= this.MAX_FAILURES) {
      // Parola kimlik doğrulamasına geri dön
      return { success: false, error: 'Maksimum deneme aşıldı' };
    }

    const result = await this.authenticateWithBiometrics();

    if (!result.success) {
      this.failureCount++;
    } else {
      this.failureCount = 0;
    }

    return result;
  }
}

iOS’ta Arka Plan Yenilemesi#

iOS’ta kritik güncellemeleri sessiz push bildirimleri taşır. Arka plan yenilemesine tek başına güvenilemez, çünkü işletim sistemi arka planda ne kadar çalışma süresi verileceğini sıkı şekilde sınırlar.

// AppDelegate.m
- (void)application:(UIApplication *)application
    didReceiveRemoteNotification:(NSDictionary *)userInfo
    fetchCompletionHandler:(void (^)(UIBackgroundFetchResult))completionHandler {
  // Token yenilemesini tetikle
  [RNBackgroundFetch performFetchWithCompletionHandler:completionHandler];
}

Ağ Durumu İşleme#

Kararsız bir bağlantıda token yenilemesi istek ortasında başarısız olabilir. Yenileme çağrısını kuyruğa alın ve ağ izleyicisi cihazın tekrar çevrimiçi olduğunu bildirdiğinde yeniden çalıştırın.

class NetworkAwareTokenManager {
  private refreshQueue: Array<() => Promise<void>> = [];
  private isOnline = true;

  constructor() {
    NetInfo.addEventListener(state => {
      const wasOffline = !this.isOnline;
      this.isOnline = state.isConnected;

      if (wasOffline && this.isOnline) {
        this.processQueue();
      }
    });
  }

  async queueRefresh(): Promise<void> {
    return new Promise((resolve, reject) => {
      const task = async () => {
        try {
          await this.performTokenRefresh();
          resolve();
        } catch (error) {
          reject(error);
        }
      };

      if (this.isOnline) {
        task();
      } else {
        this.refreshQueue.push(task);
      }
    });
  }

  private async processQueue(): Promise<void> {
    while (this.refreshQueue.length > 0) {
      const task = this.refreshQueue.shift();
      if (task) await task();
    }
  }
}

Auth Akışını Sertleştirmek#

Token Depolama Güvenliği#

iOS ve Android’in güvenli depolama için kendi temel gereksinimleri var.

  • iOS: kSecAccessControlBiometryCurrentSet ile Keychain
  • Android: Kullanıcı kimlik doğrulaması gerektiren Android Keystore

Sertifika Sabitleme#

Auth0 endpoint’leri için sertifika sabitleme, React Native 0.78+ ve Network Security Config gerektirir.

<!-- ios/YourApp/Info.plist -->
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSPinnedDomains</key>
  <dict>
    <key>your-tenant.auth0.com</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSPinnedCAIdentities</key>
      <array>
        <dict>
          <key>SPKI-SHA256-BASE64</key>
          <string>YOUR_PIN_HERE</string>
        </dict>
      </array>
    </dict>
  </dict>
</dict>
<!-- android/app/src/main/res/xml/network_security_config.xml -->
<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">your-tenant.auth0.com</domain>
    <pin-set>
      <pin digest="SHA-256">YOUR_PIN_HERE</pin>
    </pin-set>
  </domain-config>
</network-security-config>

Jailbreak/Root Tespiti#

import JailMonkey from 'jail-monkey';

const checkDeviceSecurity = (): SecurityStatus => {
  if (JailMonkey.isJailBroken()) {
    return { secure: false, reason: 'Cihaz jailbreak/root yapılmış' };
  }

  if (JailMonkey.isDebuggedMode()) {
    return { secure: false, reason: 'Debugger tespit edildi' };
  }

  return { secure: true };
};

// Not: React Native New Architecture ile bazı tespit yöntemleri güncelleme gerektirebilir
// Hedef cihazlarda ve işletim sistemi sürümlerinde iyice test edin

Token Rotasyonu#

// Auth0 Dashboard > Applications > Settings > Refresh Token Rotation
{
  "rotation": {
    "enabled": true,
    "leeway": 60
  }
}

Güvenli İletişim#

Her token işlemi şifreli bir kanaldan geçmeli.

class SecureAPIClient {
  private async makeSecureRequest(url: string, options: RequestInit): Promise<Response> {
    const tokens = await SecureStorage.getTokens();

    if (!tokens || !tokens.accessToken) {
      throw new Error('Geçerli access token yok');
    }

    const response = await fetch(url, {
      ...options,
      headers: {
        ...options.headers,
        'Authorization': `Bearer ${tokens.accessToken}`,
        'X-Request-ID': generateRequestId(),
        'X-App-Version': getAppVersion()
      }
    });

    if (response.status === 401) {
      // Token süresi dolmuş olabilir, yenilemeyi dene
      await TokenManager.performTokenRefresh();

      // İsteği yeni token ile tekrar dene
      const newTokens = await SecureStorage.getTokens();
      return fetch(url, {
        ...options,
        headers: {
          ...options.headers,
          'Authorization': `Bearer ${newTokens.accessToken}`
        }
      });
    }

    return response;
  }
}

Yenileme ve Depolama Yükünü Azaltmak#

Token Önbellekleme Stratejisi#

class TokenCache {
  private cache: Map<string, { value: any; expires: number }> = new Map();

  set(key: string, value: any, ttl: number): void {
    this.cache.set(key, {
      value,
      expires: Date.now() + ttl
    });
  }

  get(key: string): any | null {
    const item = this.cache.get(key);

    if (!item) return null;

    if (Date.now() > item.expires) {
      this.cache.delete(key);
      return null;
    }

    return item.value;
  }
}

TTL’li bellek içi önbellek gibi bu yapı, her çağrıda Keychain’i tekrar okumayı önler.

Toplu Token İşlemleri#

class BatchedSecureStorage {
  private pendingWrites: Map<string, any> = new Map();
  private writeTimer: NodeJS.Timeout | null = null;

  async set(key: string, value: any): Promise<void> {
    this.pendingWrites.set(key, value);
    this.scheduleWrite();
  }

  private scheduleWrite(): void {
    if (this.writeTimer) return;

    this.writeTimer = setTimeout(() => {
      this.flushWrites();
      this.writeTimer = null;
    }, 100);
  }

  private async flushWrites(): Promise<void> {
    const writes = Array.from(this.pendingWrites.entries());
    this.pendingWrites.clear();

    await Promise.all(
      writes.map(([key, value]) =>
        Keychain.setInternetCredentials(this.SERVICE, key, value)
      )
    );
  }
}

Yazmaları böyle toplulaştırmak uygulamanın Keychain’e ne sıklıkla dokunduğunu azaltır.

Önleyici Token Yenileme#

Süresi dolmadan yenilenen bir token, çağrıyı eş zamanlı bir round trip’te asla bloklamaz.

class PreemptiveTokenManager {
  private readonly PREEMPTIVE_REFRESH_WINDOW = 10 * 60 * 1000; // 10 dakika

  async getValidToken(): Promise<string> {
    const tokens = await SecureStorage.getTokens();

    if (!tokens) {
      throw new Error('Token mevcut değil');
    }

    const timeUntilExpiry = tokens.expiresAt - Date.now();

    // Pencere içindeyse önleyici olarak yenile
    if (timeUntilExpiry < this.PREEMPTIVE_REFRESH_WINDOW) {
      // Yenilemenin tamamlanmasını bekleme
      this.performTokenRefresh().catch(console.error);
    }

    return tokens.accessToken;
  }
}

Varsayılanı Genişletmek#

Çekirdek akış oturunca birkaç ekleme onu tamamlar:

  • Çok faktörlü kimlik doğrulama (TOTP veya SMS tabanlı 2FA)
  • Cihaz güveni: parmak izi ve güvenilir cihaz yönetimi
  • Oturum analitiği: süre, yenileme kalıpları ve güvenlik olayları
  • Çevrimdışı destek: yerel token doğrulama ve senkronizasyon kuyruğu
  • Uzun süren oturumlar için biyometrik yedek mekanizma ve değişiklik algılama

Bunu production’a taşımadan önce Auth0 tenant’ını mobil için yapılandırın, token yenileme hataları için izleme kurun, hata takibi ve uyarı sistemini devreye alın, güvenlik olaylarını loglayın ve uygulamanın gerçekten hedeflediği cihaz ve işletim sistemi sürümlerinde test edin.

Sonuç#

Bir Auth0 tenant’ıyla konuşan çoğu React Native istemcisinde varsayılan ayakta kalır. Kararsız ağları, uzun süre arka planda kalmayı ve iptal edilmiş refresh token’ları, kullanıcıyı gereksiz yere giriş ekranına atmadan atlatır.

İki durumda bu varsayılandan sapın. Uygulamanın uzun süre tamamen çevrimdışı kullanılabilir kalması gerekiyorsa kısa ömürlü access token’lar sizi yerel token doğrulamaya ve bir senkronizasyon kuyruğuna zorlar; bu da depolama tasarımını değiştirir. Arka plan yenilemesi bir kolaylık değil de sistemin taşıyıcı parçasıysa iOS arka plan fetch’i bu yükü kaldıramaz: o yolu sessiz push bildirimlerine taşıyın.

Kaynaklar#

İlgili yazılar