Skip to content
~/sph.sh

React Native'de Auth0 ve Biyometrik Kimlik Doğrulama ile Gerçek Dünya Session Yönetimi

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

Özet

Mobil uygulamalarda oturum yönetimi, web geliştiricilerinin nadiren karşılaştığı benzersiz zorluklar sunar. Mobil uygulamalar, kusursuz bir kullanıcı deneyimi sağlarken arka plan durumlarını, ağ kesintilerini, biyometrik kimlik doğrulamayı ve platforma özgü güvenlik kısıtlamalarını yönetmelidir. Bu kılavuz, Auth0 ve biyometrik kimlik doğrulama kullanarak React Native uygulamalarında güçlü oturum yönetimi uygulamak için etkili bir yaklaşım sunar.

Bu eğitimde, token yaşam döngüsünü yöneten, biyometrik kimlik doğrulamayı uygulayan, arka plan işlemlerini yöneten ve edge case'lerden zarif bir şekilde kurtaran eksiksiz bir oturum yönetim sistemi oluşturacaksınız. Uygulama, gerçek dünya senaryolarına odaklanır ve startup MVP'lerinden kurumsal uygulamalara kadar ölçeklenen, etkili kalıplar sağlar.

Ön Gereksinimler

Bu uygulamaya başlamadan önce, aşağıdakilere sahip olduğunuzdan emin olun:

  • React Native geliştirme ortamı yapılandırılmış (React Native 0.73+)
  • 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:

json
{  "react-native": "^0.73.0",  "react-native-auth0": "^3.2.0",  "react-native-biometrics": "^3.0.1",  "react-native-keychain": "^8.2.0",  "@react-native-async-storage/async-storage": "^1.21.0",  "@react-native-community/netinfo": "^11.2.0",  "react-native-background-fetch": "^4.2.0"}

Adım 1: Session Yönetimi Mimarisini Anlamak

Kod uygulamasına başlamadan önce, uygulamamıza rehberlik edecek temel kavramları ve mimariyi oluşturalım.

Session Durumları ve Yaşam Döngüsü

Mobil uygulamalar, çeşitli kimlik doğrulama senaryolarını yönetmek için karmaşık durum yönetimi gerektirir:

typescript
// types/AuthState.tsexport 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

Üç token sistemini anlamak kritiktir:

  1. Access Token (15 dakika): API yetkilendirmesi için kullanılır
  2. Refresh Token (30 gün): Yeni access token'ları almak için kullanılır
  3. ID Token (1 saat): Kullanıcı profil bilgilerini içerir

Adım 2: Auth0 Entegrasyonunu Kurmak

Tüm kimlik doğrulama işlemlerini yöneten güçlü bir Auth0 servisi oluşturun:

typescript
// services/auth/AuthService.tsimport 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',        // Gelişmiş güvenlik için refresh token rotasyonunu etkinleştirin        parameters: {          device: 'mobile',          rotation: 'rotating'        }      });
      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 {      // Auth0 SDK v3 rotasyon desteğiyle yerleşik token yenilemeye sahip      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

Token'ları platforma özgü güvenli depolama kullanarak güvenli bir şekilde saklayın:

typescript
// services/storage/SecureStorage.tsimport * 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,          accessGroup: this.KEYCHAIN_ACCESS_GROUP,          authenticatePrompt: 'Hesabınıza erişmek için kimlik doğrulayın',          authenticationPromptType: Keychain.AUTHENTICATION_TYPE.BIOMETRICS        }      );
      // 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('Token'ları güvenli bir ş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('Token'lar 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

Biyometrik kimlik doğrulama için modern react-native-biometrics kütüphanesini kullanın:

typescript
// services/auth/BiometricService.tsimport ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';import SecureStorage from '../storage/SecureStorage';
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

Yeniden deneme mantığı ve ağ farkındalığı ile otomatik token yenilemeyi uygulayın:

typescript
// services/auth/TokenManager.tsimport 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);    });  }
  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

Uygulama durum değişikliklerini yönetin ve boşta kalma zaman aşımını uygulayın:

typescript
// hooks/useAppStateAuth.tsimport { 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 taze 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

Tüm kimlik doğrulama durumunu yöneten kapsamlı bir auth context oluşturun:

typescript
// contexts/AuthContext.tsximport React, { createContext, useContext, useEffect, useReducer } from 'react';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';
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

Kimlik doğrulama durumuna saygı duyan navigasyon korumalarını uygulayın:

typescript
// navigation/AuthNavigator.tsximport 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 aktarimport 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>  );};

Yaygın Sorunları Giderme

Token Yenileme Hataları

Sorun: Token yenilemesi "invalid_grant" hatasıyla başarısız oluyor.

Çözüm: Bu genellikle refresh token'ın iptal edildiğinde veya süresi dolduğunda meydana gelir. Uygun hata işlemeyi uygulayın:

typescript
// TokenManager'daif (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ü

Sorun: Uygulama sürekli biyometrik kimlik doğrulama istiyor.

Çözüm: Bir geri çekilme mekanizması uygulayın:

typescript
class BiometricService {  private failureCount = 0;  private readonly MAX_FAILURES = 3;
  async authenticateWithBiometrics(): 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.performBiometricAuth();
    if (!result.success) {      this.failureCount++;    } else {      this.failureCount = 0;    }
    return result;  }}

iOS'ta Arka Plan Yenilemesi

Sorun: iOS'ta arka plan yenilemesi güvenilir şekilde çalışmıyor.

Çözüm: iOS'un katı sınırlamaları vardır. Kritik güncellemeler için sessiz push bildirimlerini kullanın:

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

Ağ Durumu İşleme

Sorun: Ağ kararsız olduğunda token yenilemesi başarısız oluyor.

Çözüm: Ağ izleme ile kuyruk tabanlı yeniden deneme uygulayın:

typescript
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();    }  }}

Güvenlik En İyi Uygulamaları

1. Token Depolama Güvenliği

Her zaman platforma özgü güvenli depolama kullanın:

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

2. Sertifika Sabitleme

Auth0 endpoint'leri için sertifika sabitleme uygulayın:

typescript
// 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>

3. Jailbreak/Root Tespiti

Güvenliği ihlal edilmiş cihazları tespit edin ve işlevselliği sınırlayın:

typescript
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 };};

4. Token Rotasyonu

Auth0'da refresh token rotasyonunu etkinleştirin:

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

5. Güvenli İletişim

Tüm token işlemleri için şifreli kanallar kullanın:

typescript
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;  }}

Performans Optimizasyon İpuçları

1. Token Önbellekleme Stratejisi

TTL ile bellek önbellekleme uygulayın:

typescript
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;  }}

2. Toplu Token İşlemleri

İşlemleri toplu hale getirerek Keychain erişimini azaltın:

typescript
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)      )    );  }}

3. Önleyici Token Yenileme

Gecikmeleri önlemek için token'ları süreleri dolmadan yenileyin:

typescript
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;  }}

Sonraki Adımlar

Bu oturum yönetim sistemini uyguladıktan sonra, şu geliştirmeleri düşünün:

  1. Çok Faktörlü Kimlik Doğrulama: TOTP veya SMS tabanlı 2FA ekleyin
  2. Cihaz Güveni: Cihaz parmak izi ve güvenilir cihaz yönetimi uygulayın
  3. Oturum Analitiği: Oturum süresi, yenileme kalıpları ve güvenlik olaylarını takip edin
  4. Çevrimdışı Destek: Çevrimdışı token doğrulama ve çevrimiçi olduğunda senkronizasyon uygulayın
  5. Gelişmiş Biyometrikler: Yedek mekanizmalar ve biyometrik değişiklik algılama ekleyin

Production dağıtımı için:

  1. Mobil optimizasyon için Auth0 kiracı ayarlarını yapılandırın
  2. Token yenileme hataları için izleme kurun
  3. Uygun hata takibi ve uyarı uygulayın
  4. Güvenlik olayları için kapsamlı loglama ekleyin
  5. Çeşitli cihazlarda ve işletim sistemi sürümlerinde test edin

Sonuç

React Native'de güçlü oturum yönetimi uygulamak, mobil özgü kısıtlamaların ve güvenlik gereksinimlerinin dikkatli bir şekilde değerlendirilmesini gerektirir. Bu uygulama, token yaşam döngüsünün karmaşıklıklarını, biyometrik kimlik doğrulamayı ve production'da sıklıkla sorunlara neden olan edge case'leri ele alan production'a hazır bir temel sağlar.

Bu uygulamadan çıkan temel içgörüler:

  • Manuel yönetim yerine Auth0'ın yerleşik token rotasyon özelliklerini kullanın
  • Üstel geri çekilme ile uygun yeniden deneme mantığı uygulayın
  • Ağ durumu değişikliklerini zarif bir şekilde ele alın
  • Platforma özgü güvenli depolama mekanizmaları kullanın
  • Kullanıcı kesintisini önlemek için önleyici token yenileme uygulayın

Güvenliğin sürekli bir süreç olduğunu unutmayın. Düzenli güvenlik denetimleri, bağımlılık güncellemeleri ve kimlik doğrulama kalıplarının izlenmesi, güvenli ve güvenilir bir kimlik doğrulama sistemi sürdürmeye yardımcı olacaktır.

İlgili Yazılar