Module 11 — Security & Storage

Phải có ⏱ 6-8 giờ 📋 Prerequisites: Module 01-04, 07

🎯 Mục tiêu học tập

📖 Hướng dẫn học

1 Đọc OWASP Mobile Top 10owasp.org/www-project-mobile-top-10
Đọc kỹ từng mục, ghi chú lại cách từng mục áp dụng vào React Native app.
Thời gian: ~1.5 giờ
2 Implement react-native-keychainGitHub: react-native-keychain
Setup project, lưu/đọc tokens qua Keychain (iOS) và Keystore (Android). So sánh với AsyncStorage bằng cách inspect storage.
Thời gian: ~1.5 giờ
3 Setup MMKVGitHub: react-native-mmkv
Thay thế AsyncStorage bằng MMKV. Benchmark performance difference. Tích hợp với Zustand/Redux persist.
Thời gian: ~1.5 giờ
4 Implement offline-first với React Query persist
Đọc TanStack Query Persist docs. Setup persistQueryClient với MMKV storage adapter. Test bằng cách tắt network.
Thời gian: ~1.5 giờ

🧭 Vòng lặp học module này

📚 Lý thuyết chi tiết

1. Secure Token Storage: Keychain/Keystore vs AsyncStorage

KHÔNG BAO GIỜ lưu tokens, passwords, API keys vào AsyncStorage.

AsyncStorage lưu data dưới dạng plaintext trong SQLite database (Android) hoặc plist file (iOS). Bất kỳ ai có physical access hoặc root access đều đọc được. Trên Android, file nằm ở /data/data/com.yourapp/databases/RKStorage — dùng adb shell là đọc thẳng.

Keychain (iOS) / Keystore (Android) là hardware-backed secure storage do OS cung cấp:

Cài đặt và sử dụng react-native-keychain:

import * as Keychain from 'react-native-keychain';

const saveToken = async (token: string): Promise<boolean> => {
  const result = await Keychain.setGenericPassword('auth_token', token, {
    accessible: Keychain.ACCESSIBLE.WHEN_UNLOCKED_THIS_DEVICE_ONLY,
    securityLevel: Keychain.SECURITY_LEVEL.SECURE_HARDWARE,
  });
  return result !== false;
};

const getToken = async (): Promise<string | null> => {
  const credentials = await Keychain.getGenericPassword();
  if (credentials) {
    return credentials.password;
  }
  return null;
};

const removeToken = async (): Promise<void> => {
  await Keychain.resetGenericPassword();
};

Option WHEN_UNLOCKED_THIS_DEVICE_ONLY đảm bảo token chỉ accessible khi device unlocked và không được sync qua iCloud backup. SECURE_HARDWARE yêu cầu hardware-backed encryption trên Android.

2. MMKV vs AsyncStorage

Tiêu chíAsyncStorageMMKV
PerformanceChậm (async, serialization overhead)Nhanh ~30x (memory-mapped, synchronous)
APIAsync only (await)Sync + Async
Data typesString only (phải JSON.stringify)String, number, boolean, Buffer
EncryptionKhôngCó (AES-CFB)
Size limit6MB default (Android)Không giới hạn
Multi-processKhông
Storage engineSQLite (Android) / file (iOS)Memory-mapped file (mmap)
Use caseLegacy apps, simple needsProduction apps, settings, cache

Lưu ý: MMKV nhanh nhưng vẫn KHÔNG phải nơi lưu tokens/secrets. MMKV encrypted mode dùng app-level key — không phải hardware-backed như Keychain/Keystore. Dùng MMKV cho user preferences, cache, feature flags. Dùng Keychain cho tokens, passwords, biometric data.

Setup MMKV:

import { MMKV } from 'react-native-mmkv';

const storage = new MMKV({
  id: 'app-storage',
  encryptionKey: 'your-encryption-key',
});

storage.set('user.theme', 'dark');
storage.set('user.onboarded', true);
storage.set('cache.lastFetch', Date.now());

const theme = storage.getString('user.theme');
const onboarded = storage.getBoolean('user.onboarded');

storage.delete('cache.lastFetch');

const allKeys = storage.getAllKeys();

Tích hợp MMKV với Zustand persist:

import { create } from 'zustand';
import { persist, createJSONStorage, StateStorage } from 'zustand/middleware';
import { MMKV } from 'react-native-mmkv';

const mmkv = new MMKV({ id: 'zustand-storage' });

const zustandStorage: StateStorage = {
  setItem: (name: string, value: string) => {
    mmkv.set(name, value);
  },
  getItem: (name: string) => {
    return mmkv.getString(name) ?? null;
  },
  removeItem: (name: string) => {
    mmkv.delete(name);
  },
};

interface SettingsState {
  theme: 'light' | 'dark';
  language: string;
  setTheme: (theme: 'light' | 'dark') => void;
  setLanguage: (lang: string) => void;
}

const useSettingsStore = create<SettingsState>()(
  persist(
    (set) => ({
      theme: 'light',
      language: 'vi',
      setTheme: (theme) => set({ theme }),
      setLanguage: (language) => set({ language }),
    }),
    {
      name: 'settings',
      storage: createJSONStorage(() => zustandStorage),
    }
  )
);

3. Offline-first Patterns

Offline-first là kiến trúc ưu tiên dữ liệu local trước, sync với server khi có network. User experience không bị gián đoạn dù mất mạng.

3 cấp độ offline support:

React Query Persist với MMKV

import { QueryClient } from '@tanstack/react-query';
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';
import { MMKV } from 'react-native-mmkv';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      gcTime: 1000 * 60 * 60 * 24,
      staleTime: 1000 * 60 * 5,
    },
  },
});

const mmkv = new MMKV({ id: 'query-cache' });

const persister = createSyncStoragePersister({
  storage: {
    getItem: (key: string) => mmkv.getString(key) ?? null,
    setItem: (key: string, value: string) => mmkv.set(key, value),
    removeItem: (key: string) => mmkv.delete(key),
  },
});

function App() {
  return (
    <PersistQueryClientProvider
      client={queryClient}
      persistOptions={{ persister, maxAge: 1000 * 60 * 60 * 24 }}
    >
      <MainApp />
    </PersistQueryClientProvider>
  );
}

Offline Mutations Queue

import { useMutation, useQueryClient, onlineManager } from '@tanstack/react-query';
import NetInfo from '@react-native-community/netinfo';

onlineManager.setEventListener((setOnline) => {
  return NetInfo.addEventListener((state) => {
    setOnline(!!state.isConnected);
  });
});

const useCreatePost = () => {
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: (newPost: CreatePostInput) => api.createPost(newPost),
    onMutate: async (newPost) => {
      await queryClient.cancelQueries({ queryKey: ['posts'] });
      const previousPosts = queryClient.getQueryData<Post[]>(['posts']);
      queryClient.setQueryData<Post[]>(['posts'], (old) => [
        ...(old ?? []),
        { ...newPost, id: `temp-${Date.now()}`, pending: true },
      ]);
      return { previousPosts };
    },
    onError: (_err, _newPost, context) => {
      queryClient.setQueryData(['posts'], context?.previousPosts);
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['posts'] });
    },
  });
};

Pattern trên sử dụng optimistic update: UI cập nhật ngay lập tức, nếu mutation fail thì rollback. Kết hợp với onlineManager, mutations sẽ được queue khi offline và tự retry khi online.

Local Database Options

LibraryTypeSyncPerformanceUse case
SQLite (expo-sqlite)RelationalManualTốtComplex queries, existing SQL skills
WatermelonDBRelational (lazy)Built-in syncRất tốt (lazy loading)Large datasets, offline-first apps
RealmObject DBAtlas SyncRất tốtMongoDB ecosystem
MMKVKey-ValueKhôngCực nhanhSettings, simple cache

4. Certificate Pinning

Certificate Pinning đảm bảo app chỉ trust certificate cụ thể của server, không phải bất kỳ CA nào trong trust store. Chống man-in-the-middle (MITM) attacks ngay cả khi attacker install malicious CA certificate trên device.

Tại sao cần:

Implement certificate pinning với react-native-ssl-pinning:

import { fetch as sslFetch } from 'react-native-ssl-pinning';

const fetchWithPinning = async (url: string) => {
  const response = await sslFetch(url, {
    method: 'GET',
    timeoutInterval: 10000,
    sslPinning: {
      certs: ['server_cert'],
    },
    headers: {
      'Content-Type': 'application/json',
    },
  });
  return response.json();
};

Cách khác: dùng TrustKit (iOS) hoặc cấu hình network_security_config.xml (Android):

<network-security-config>
  <domain-config>
    <domain includeSubdomains="true">api.yourapp.com</domain>
    <pin-set expiration="2025-12-31">
      <pin digest="SHA-256">AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=</pin>
      <pin digest="SHA-256">BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=</pin>
    </pin-set>
  </domain-config>
</network-security-config>

Rủi ro của Certificate Pinning: Nếu certificate rotate mà app chưa update pin → app sẽ không connect được. Luôn pin backup certificate và có mechanism để update pins remotely (hoặc dùng public key pinning thay vì certificate pinning).

5. Biometric Authentication

import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';

const biometrics = new ReactNativeBiometrics();

const checkBiometricAvailability = async () => {
  const { available, biometryType } = await biometrics.isSensorAvailable();

  if (!available) {
    return { supported: false, type: null };
  }

  return {
    supported: true,
    type: biometryType,
    isFaceID: biometryType === BiometryTypes.FaceID,
    isFingerprint: biometryType === BiometryTypes.Biometrics,
  };
};

const authenticateWithBiometrics = async (): Promise<boolean> => {
  const { success } = await biometrics.simplePrompt({
    promptMessage: 'Xác nhận danh tính',
    cancelButtonText: 'Hủy',
  });
  return success;
};

const createBiometricKey = async () => {
  const { publicKey } = await biometrics.createKeys();
  await sendPublicKeyToServer(publicKey);
};

const signWithBiometrics = async (payload: string) => {
  const { signature } = await biometrics.createSignature({
    promptMessage: 'Xác nhận giao dịch',
    payload,
  });
  return signature;
};

createKeys() tạo RSA key pair trong Secure Enclave (iOS) / Keystore (Android). Private key không bao giờ rời khỏi device. createSignature() ký data bằng private key sau khi user xác thực biometrics — server verify bằng public key.

6. Root/Jailbreak Detection

Rooted (Android) / Jailbroken (iOS) devices có nguy cơ bảo mật cao hơn: app sandbox bị phá, attacker có thể đọc Keychain, hook vào app process (Frida, Xposed).

Phương pháp detect:

import JailMonkey from 'jail-monkey';

const performSecurityChecks = () => {
  const isRooted = JailMonkey.isJailBroken();
  const canMockLocation = JailMonkey.canMockLocation();
  const isDebugMode = JailMonkey.isDebuggedMode();
  const isOnExternalStorage = JailMonkey.isOnExternalStorage();

  const threats: string[] = [];

  if (isRooted) threats.push('ROOTED_DEVICE');
  if (canMockLocation) threats.push('MOCK_LOCATION');
  if (isDebugMode) threats.push('DEBUG_MODE');
  if (isOnExternalStorage) threats.push('EXTERNAL_STORAGE');

  return {
    isSecure: threats.length === 0,
    threats,
  };
};

const enforceSecurityPolicy = (threats: string[]) => {
  if (threats.includes('ROOTED_DEVICE')) {
    return 'block';
  }
  if (threats.includes('DEBUG_MODE')) {
    return 'warn';
  }
  return 'allow';
};

Cân nhắc: Root detection không phải giải pháp tuyệt đối — Magisk Hide, Frida scripts có thể bypass. Nên kết hợp nhiều checks (defense in depth) và không chỉ dựa vào client-side validation. Banking apps thường block rooted devices; e-commerce apps có thể chỉ warn.

7. OWASP Mobile Top 10

M1 — Improper Credential Usage

Hardcode API keys, credentials trong source code. Dùng react-native-config cho environment variables, lưu secrets ở server-side.

M2 — Inadequate Supply Chain Security

Third-party libraries có thể chứa malicious code. Luôn audit dependencies (npm audit), pin versions, dùng lockfile.

M3 — Insecure Authentication/Authorization

Weak password policies, thiếu MFA, không validate JWT expiry ở client. Implement token refresh flow đúng cách, check token expiry trước mỗi request.

M4 — Insufficient Input/Output Validation

SQL injection, XSS qua WebView. Validate mọi user input ở cả client và server. Dùng parameterized queries cho SQLite.

M5 — Insecure Communication

HTTP thay vì HTTPS, thiếu certificate pinning. Enforce TLS 1.2+, implement certificate pinning cho critical APIs.

M6 — Inadequate Privacy Controls

Collect data không cần thiết, không anonymize PII. Chỉ collect data cần thiết, encrypt PII, comply với GDPR/CCPA.

M7 — Insufficient Binary Protections

Reverse engineering, code tampering. Dùng ProGuard/R8 obfuscation, Hermes bytecode (RN), integrity checks.

M8 — Security Misconfiguration

Debug mode trong production, backup enabled, cleartext traffic allowed. Disable debug flags, configure android:allowBackup="false".

M9 — Insecure Data Storage

Sensitive data trong AsyncStorage, logs, screenshots. Dùng Keychain/Keystore, disable screenshots cho sensitive screens.

M10 — Insufficient Cryptography

Weak algorithms (MD5, SHA1 cho passwords), hardcoded encryption keys. Dùng AES-256, bcrypt/scrypt cho passwords, key management qua Keystore.

8. Encryption

Data at Rest

Data lưu trên device phải được encrypt:

import { MMKV } from 'react-native-mmkv';

const encryptedStorage = new MMKV({
  id: 'encrypted-store',
  encryptionKey: 'your-256-bit-key-here',
});

encryptedStorage.set('sensitiveData', JSON.stringify({
  cardLastFour: '4242',
  expiryMonth: 12,
}));

Data in Transit

Mọi network communication phải qua HTTPS (TLS 1.2+):

Block cleartext traffic trên Android:

<network-security-config>
  <base-config cleartextTrafficPermitted="false">
    <trust-anchors>
      <certificates src="system" />
    </trust-anchors>
  </base-config>
</network-security-config>

9. Environment Variables

import Config from 'react-native-config';

const API_BASE_URL = Config.API_BASE_URL;
const ANALYTICS_KEY = Config.ANALYTICS_KEY;
const ENVIRONMENT = Config.ENVIRONMENT;

File .env:

API_BASE_URL=https://api.yourapp.com
ANALYTICS_KEY=UA-XXXXXXXXX
ENVIRONMENT=production

QUAN TRỌNG: react-native-config inject biến vào app bundle — chúng KHÔNG an toàn, có thể decompile ra. KHÔNG lưu secrets (API secret keys, database passwords) trong .env. Chỉ dùng cho public configuration (API URLs, feature flags, analytics IDs). Secrets phải lưu ở server và truy cập qua authenticated API.

10. Screen Capture Prevention

Ngăn screenshot/screen recording cho sensitive screens (banking, passwords):

import { Platform } from 'react-native';
import { useEffect } from 'react';
import { useFocusEffect } from '@react-navigation/native';
import ScreenCapture from 'react-native-screen-capture';

const usePreventScreenCapture = () => {
  useFocusEffect(
    useCallback(() => {
      ScreenCapture.enableSecureView();
      return () => {
        ScreenCapture.disableSecureView();
      };
    }, [])
  );
};

Trên Android, cơ chế này set FLAG_SECURE trên Window. Trên iOS, overlay hidden text field để trigger DRM protection. Expo users có thể dùng expo-screen-capture.

11. ProGuard/R8 Obfuscation

Obfuscation làm cho code khó đọc hơn sau khi decompile. R8 (replacement cho ProGuard) là default trong Android Gradle Plugin:

Enable trong android/app/build.gradle:

android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

ProGuard rules cho React Native (proguard-rules.pro):

-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.** { *; }
-keep class com.facebook.jni.** { *; }

-dontwarn com.facebook.react.**

-keepclassmembers class * {
    @com.facebook.react.uimanager.annotations.ReactProp *;
}

-keep,allowobfuscation @interface com.facebook.proguard.annotations.DoNotStrip
-keep,allowobfuscation @interface com.facebook.proguard.annotations.KeepGettersAndSetters

-keep @com.facebook.proguard.annotations.DoNotStrip class *
-keepclassmembers class * {
    @com.facebook.proguard.annotations.DoNotStrip *;
}

Hermes bytecode: React Native với Hermes compiler đã convert JS thành bytecode — khó đọc hơn plain JS nhưng không phải true obfuscation. Kết hợp với R8 cho native code và Hermes cho JS code để có protection tốt nhất.

❓ Câu hỏi phỏng vấn + Đáp án

🛠️ Debug playbook: security/storage bug

Triệu chứngKiểm traFix thường gặp
User mới thấy data user cũQuery cache, persisted store, secure storage, navigation resetLogout clear token/cache/store/offline queue và reset nav
Token vẫn tồn tại sau logoutKeychain/SecureStore remove có await chưa, error bị nuốt khôngAwait clear, handle failure, verify storage empty
Secret bị lộ trong APK/IPA.env, BuildConfig, bundle stringsKhông nhúng secret client-side; chuyển secret về server
Pinning làm app mất kết nốiCert rotate, pin backup, domain configCó kế hoạch rotate và backup pin

Khung trả lời phỏng vấn: Mobile security không có một lớp bảo vệ tuyệt đối. Trả lời theo data sensitivity, storage choice, transport security, logout cleanup, device compromise và operational risk.

Q1: Tại sao không được dùng AsyncStorage để lưu access tokens? Giải pháp thay thế?

AsyncStorage lưu data dưới dạng plaintext trong SQLite (Android) hoặc plist file (iOS). Bất kỳ ai có physical access, root access, hoặc adb debug access đều đọc được. Trên Android, file nằm ở /data/data/<package>/databases/RKStorage.

Giải pháp: Dùng react-native-keychain để lưu vào iOS Keychain / Android Keystore. Đây là hardware-backed storage, encrypted bởi OS, sandboxed per-app. Có thể thêm biometric gating (yêu cầu FaceID/fingerprint trước khi trả token).

Phân loại storage:

  • Keychain/Keystore: tokens, passwords, encryption keys
  • MMKV (encrypted): user preferences, feature flags, non-sensitive cache
  • AsyncStorage: chỉ dùng cho legacy code hoặc data hoàn toàn non-sensitive
Q2: Certificate Pinning là gì? Tại sao cần và có những rủi ro gì?

Certificate Pinning là kỹ thuật đảm bảo app chỉ chấp nhận certificate cụ thể khi kết nối HTTPS, thay vì trust bất kỳ CA nào trong system trust store.

Tại sao cần: Ngăn MITM attacks — attacker install rogue CA certificate trên device (qua corporate proxy, malware) có thể intercept HTTPS traffic. Với pinning, app sẽ reject connections dù certificate valid theo CA nhưng không match pin.

Rủi ro:

  • Certificate rotation: Nếu server rotate certificate mà app chưa update pin → app bị block hoàn toàn. Cần pin backup certificate
  • Debugging khó: Không thể dùng proxy tools (Charles, Flipper) để debug network → cần disable pinning trong debug builds
  • Public key pinning tốt hơn certificate pinning vì public key ít thay đổi hơn certificate
Q3: So sánh MMKV với AsyncStorage. Khi nào dùng cái nào?

Performance: MMKV nhanh hơn ~30x nhờ memory-mapped file (mmap) thay vì SQLite queries. MMKV hỗ trợ synchronous API — không cần await, không block JS thread vì read từ memory.

AsyncStorage dùng khi: legacy project không thể migrate, hoặc cần cross-platform web compatibility (AsyncStorage có web adapter). Hầu hết mọi trường hợp khác nên dùng MMKV.

MMKV dùng cho: user settings, theme, language, onboarding state, cache, feature flags. Tích hợp tốt với Zustand persist, Redux persist.

Không dùng cả hai cho: tokens, passwords, secrets → dùng Keychain/Keystore.

Q4: Giải thích offline-first architecture. Implement thế nào với React Query?

Offline-first ưu tiên data local, sync với server khi có mạng. User không thấy loading states khi mở app (data từ cache).

React Query implement:

  • persistQueryClient: serialize query cache vào MMKV, restore khi app restart
  • staleTime: set thời gian data considered fresh (e.g., 5 phút)
  • gcTime: set thời gian cache giữ data (e.g., 24 giờ)
  • onlineManager: integrate NetInfo để detect network state, auto-pause/resume queries
  • Optimistic updates: update cache ngay khi mutate, rollback nếu server reject
  • Mutation queue: mutations khi offline được queue, auto-retry khi online

Cho complex apps cần full offline (e.g., field service app), dùng WatermelonDB hoặc PowerSync thay vì chỉ React Query persist.

Q5: OWASP Mobile Top 10 — liệt kê và giải thích 3 mục quan trọng nhất cho RN app?

M9 — Insecure Data Storage: Lưu tokens/passwords vào AsyncStorage, logs chứa sensitive data, clipboard exposure. Giải pháp: Keychain/Keystore cho secrets, MMKV encrypted cho preferences, không log sensitive data.

M5 — Insecure Communication: HTTP requests, thiếu certificate pinning, trust bất kỳ CA. Giải pháp: enforce HTTPS only, certificate pinning cho critical APIs, disable cleartext traffic.

M3 — Insecure Authentication/Authorization: Token không expire, thiếu refresh flow, không validate server-side. Giải pháp: short-lived access tokens + refresh tokens, validate JWT expiry client-side, implement proper token refresh interceptor.

Q6: Làm thế nào để detect root/jailbreak? Có đáng tin không?

Phương pháp: Dùng jail-monkey hoặc react-native-device-info để check:

  • Sự tồn tại của Cydia (iOS), Magisk/SuperSU (Android)
  • File system writability ở root paths (/system, /private)
  • SU binary existence
  • App đang chạy trong emulator
  • Debugger attached

Độ tin cậy: Thấp. Magisk Hide, Frida scripts có thể bypass hầu hết client-side checks. Root detection chỉ là 1 layer trong defense-in-depth strategy. Không nên dùng root detection như primary security measure — phải combine với server-side validation, certificate pinning, code obfuscation.

Policy: Banking apps block rooted devices. E-commerce apps warn. Internal tools có thể ignore.

Q7: Environment variables trong React Native hoạt động thế nào? Có an toàn không?

react-native-config đọc file .env và inject vào app bundle tại build time. Trên Android, values được inject vào BuildConfigres/values. Trên iOS, inject vào Info.plist.

KHÔNG AN TOÀN cho secrets. Bất kỳ giá trị nào trong .env đều có thể extract từ app bundle sau khi decompile. APK/IPA chỉ là ZIP file — ai cũng có thể giải nén và tìm strings.

Chỉ dùng cho: API base URLs, analytics IDs, feature flags, environment identifiers (staging/production).

Secrets phải: Lưu ở server, truy cập qua authenticated API. Hoặc dùng build-time CI/CD secrets injection + native code embedding (vẫn có thể extract nhưng khó hơn).

Q8: Giải thích data encryption at rest vs in transit. Implement thế nào trong RN?

At rest (dữ liệu lưu trữ):

  • Keychain/Keystore: auto-encrypted bởi OS hardware
  • MMKV: AES encryption mode (app-level key)
  • SQLite: dùng SQLCipher cho encrypted DB
  • Files: encrypt trước khi write bằng react-native-aes-crypto

In transit (dữ liệu truyền tải):

  • HTTPS/TLS 1.2+ cho mọi API calls
  • Certificate pinning cho APIs critical
  • iOS App Transport Security enforce HTTPS by default
  • Android network_security_config.xml block cleartext
  • End-to-end encryption cho messaging (Signal Protocol)

Nguyên tắc: Sensitive data phải encrypted cả khi lưu trữ và khi truyền tải. Dùng strong algorithms (AES-256, RSA-2048+). Không implement crypto algorithms tự — dùng OS/library cung cấp.

🏋️ Bài tập thực hành

Bài tập: Implement Secure Auth Flow

Xây dựng complete authentication flow với security best practices:

Yêu cầu:

Acceptance Criteria:

Hướng dẫn giải — Token Refresh Interceptor:
import axios, { AxiosError, InternalAxiosRequestConfig } from 'axios';
import * as Keychain from 'react-native-keychain';

let isRefreshing = false;
let failedQueue: Array<{
  resolve: (token: string) => void;
  reject: (error: Error) => void;
}> = [];

const processQueue = (error: Error | null, token: string | null) => {
  failedQueue.forEach((promise) => {
    if (error) {
      promise.reject(error);
    } else {
      promise.resolve(token!);
    }
  });
  failedQueue = [];
};

const apiClient = axios.create({
  baseURL: 'https://api.yourapp.com',
});

apiClient.interceptors.request.use(async (config: InternalAxiosRequestConfig) => {
  const credentials = await Keychain.getGenericPassword({ service: 'accessToken' });
  if (credentials) {
    config.headers.Authorization = `Bearer ${credentials.password}`;
  }
  return config;
});

apiClient.interceptors.response.use(
  (response) => response,
  async (error: AxiosError) => {
    const originalRequest = error.config as InternalAxiosRequestConfig & { _retry?: boolean };

    if (error.response?.status !== 401 || originalRequest._retry) {
      return Promise.reject(error);
    }

    if (isRefreshing) {
      return new Promise<string>((resolve, reject) => {
        failedQueue.push({ resolve, reject });
      }).then((token) => {
        originalRequest.headers.Authorization = `Bearer ${token}`;
        return apiClient(originalRequest);
      });
    }

    originalRequest._retry = true;
    isRefreshing = true;

    const refreshCreds = await Keychain.getGenericPassword({ service: 'refreshToken' });
    if (!refreshCreds) {
      processQueue(new Error('No refresh token'), null);
      isRefreshing = false;
      navigateToLogin();
      return Promise.reject(error);
    }

    const response = await axios.post('https://api.yourapp.com/auth/refresh', {
      refreshToken: refreshCreds.password,
    });

    const { accessToken, refreshToken } = response.data;
    await Keychain.setGenericPassword('token', accessToken, { service: 'accessToken' });
    await Keychain.setGenericPassword('token', refreshToken, { service: 'refreshToken' });

    processQueue(null, accessToken);
    isRefreshing = false;

    originalRequest.headers.Authorization = `Bearer ${accessToken}`;
    return apiClient(originalRequest);
  }
);

Pattern trên sử dụng queue để handle concurrent 401 responses. Khi request đầu tiên nhận 401, nó trigger refresh. Các requests tiếp theo thấy isRefreshing = true sẽ được đẩy vào queue, đợi refresh hoàn thành rồi retry với token mới.

🧩 Mini exercise bảo mật thực tế

Exercise 1: Token storage audit
Kiểm tra app sample: access token, refresh token, user profile, feature flags đang lưu ở đâu. Phân loại cái nào được lưu AsyncStorage/MMKV/Keychain.
Exercise 2: Logout sạch
Implement logout: revoke token server-side, clear secure storage, clear query cache, reset navigation, clear offline queue nhạy cảm.
Exercise 3: Threat model mini
Với app banking/health/e-commerce, liệt kê 5 dữ liệu nhạy cảm, nơi lưu, rủi ro, và mitigation tối thiểu.

⚠️ Lỗi thường gặp

🏢 Case đi làm thật

Case: User logout khỏi app, nhưng React Query cache vẫn còn profile và order cũ. Người dùng khác đăng nhập trên cùng máy thấy dữ liệu cũ trong vài giây.

Cách xử lý: Khi logout, clear secure token, reset auth store, clear query cache/persisted cache, reset navigation và hủy offline mutations chứa dữ liệu user cũ.

🧠 Ghi nhớ nhanh

Bảo mật mobile là giảm thiểu rủi ro ở nhiều lớp: lưu trữ đúng, truyền tải đúng, logout sạch, không nhúng secret, và có kế hoạch khi device/app bị compromise.

❔ Câu hỏi tự kiểm tra

1. Dữ liệu nào tuyệt đối không nên lưu AsyncStorage?
2. Logout sạch cần clear những lớp nào?
3. Vì sao certificate pinning có rủi ro vận hành?
4. Environment variable trong mobile app có phải secret không?

✅ Checklist tự đánh giá

📎 Tài nguyên

📘 OWASP Mobile Top 10 — Danh sách 10 rủi ro bảo mật mobile phổ biến nhất
📘 react-native-keychain — iOS Keychain / Android Keystore wrapper
📘 react-native-mmkv — Fastest key-value storage cho React Native
📘 React Query Persist — Official persist plugin documentation
📘 react-native-ssl-pinning — Certificate pinning library
📘 react-native-biometrics — Biometric authentication (FaceID, fingerprint)
📘 jail-monkey — Root/jailbreak detection
📺 "Mobile App Security Best Practices" — React Native security overview (YouTube)
📘 React Native Security Docs — Official security guidelines
📘 Android Network Security Config — Certificate pinning, cleartext traffic