Skip to main content
Security is critical for mobile applications. This guide covers essential security practices to protect your React Native app and user data.

Data Storage Security

Secure Storage

Never store sensitive data in AsyncStorage, which is unencrypted.

Use Encrypted Storage

npm install react-native-encrypted-storage
import EncryptedStorage from 'react-native-encrypted-storage';

// Store sensitive data
async function storeUserToken(token) {
  try {
    await EncryptedStorage.setItem(
      'user_token',
      JSON.stringify({token})
    );
  } catch (error) {
    console.error('Storage error:', error);
  }
}

// Retrieve sensitive data
async function getUserToken() {
  try {
    const session = await EncryptedStorage.getItem('user_token');
    return session ? JSON.parse(session) : null;
  } catch (error) {
    console.error('Storage error:', error);
    return null;
  }
}

// Remove sensitive data
async function clearSession() {
  try {
    await EncryptedStorage.removeItem('user_token');
  } catch (error) {
    console.error('Storage error:', error);
  }
}

Keychain/Keystore

For tokens and passwords, use platform-specific secure storage:
npm install react-native-keychain
import * as Keychain from 'react-native-keychain';

// Store credentials
async function saveCredentials(username, password) {
  await Keychain.setGenericPassword(username, password);
}

// Retrieve credentials
async function getCredentials() {
  try {
    const credentials = await Keychain.getGenericPassword();
    if (credentials) {
      return {
        username: credentials.username,
        password: credentials.password,
      };
    }
  } catch (error) {
    console.error('Keychain error:', error);
  }
  return null;
}

// Reset credentials
async function resetCredentials() {
  await Keychain.resetGenericPassword();
}
Never store passwords or API keys in plain text. Always use encrypted storage or keychain/keystore services.

What NOT to Store in AsyncStorage

// ❌ NEVER do this
await AsyncStorage.setItem('password', password);
await AsyncStorage.setItem('credit_card', cardNumber);
await AsyncStorage.setItem('api_key', apiKey);
await AsyncStorage.setItem('private_key', privateKey);

// ✅ Instead, use EncryptedStorage or Keychain
await EncryptedStorage.setItem('user_session', sessionData);
await Keychain.setGenericPassword(username, password);

Network Security

HTTPS Only

Always use HTTPS for API calls:
// ❌ Insecure
const API_URL = 'http://api.example.com';

// ✅ Secure
const API_URL = 'https://api.example.com';

Certificate Pinning

Prevent man-in-the-middle attacks by pinning certificates:
npm install react-native-ssl-pinning
import {fetch} from 'react-native-ssl-pinning';

fetch('https://api.example.com/data', {
  method: 'GET',
  sslPinning: {
    certs: ['mycert'], // cert files in ios/certs/ or android/app/src/main/assets/
  },
})
  .then(response => response.json())
  .then(data => console.log(data))
  .catch(error => console.error(error));

API Key Protection

Never hardcode API keys in your JavaScript code:
// ❌ NEVER do this - visible in bundle
const API_KEY = 'sk_live_51HxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxZZZ';

// ✅ Use environment variables
import Config from 'react-native-config';

const API_KEY = Config.API_KEY;
Setup .env:
API_KEY=your_api_key_here
API_URL=https://api.example.com
Add to .gitignore:
.env
.env.local
.env.*.local
For highly sensitive keys, use backend proxy servers instead of including them in the app.

Request Authentication

// Add authentication headers
const fetchWithAuth = async (url, options = {}) => {
  const token = await getAuthToken();
  
  return fetch(url, {
    ...options,
    headers: {
      ...options.headers,
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json',
    },
  });
};

// Usage
const response = await fetchWithAuth('https://api.example.com/user');
const data = await response.json();

Token Refresh

let authToken = null;
let refreshToken = null;
let tokenExpiry = null;

async function getValidToken() {
  // Check if token is expired or about to expire
  if (!authToken || Date.now() >= tokenExpiry - 60000) {
    await refreshAuthToken();
  }
  return authToken;
}

async function refreshAuthToken() {
  const response = await fetch('https://api.example.com/refresh', {
    method: 'POST',
    headers: {'Content-Type': 'application/json'},
    body: JSON.stringify({refreshToken}),
  });
  
  const data = await response.json();
  authToken = data.accessToken;
  refreshToken = data.refreshToken;
  tokenExpiry = Date.now() + (data.expiresIn * 1000);
  
  // Store securely
  await EncryptedStorage.setItem('tokens', JSON.stringify({
    authToken,
    refreshToken,
    tokenExpiry,
  }));
}

Authentication

Biometric Authentication

npm install react-native-biometrics
import ReactNativeBiometrics from 'react-native-biometrics';

const rnBiometrics = new ReactNativeBiometrics();

async function authenticateWithBiometrics() {
  try {
    const {available, biometryType} = await rnBiometrics.isSensorAvailable();
    
    if (!available) {
      console.log('Biometrics not available');
      return false;
    }
    
    console.log(`Biometry type: ${biometryType}`);
    // biometryType: TouchID, FaceID, or Biometrics
    
    const {success} = await rnBiometrics.simplePrompt({
      promptMessage: 'Confirm your identity',
      cancelButtonText: 'Cancel',
    });
    
    return success;
  } catch (error) {
    console.error('Biometric auth error:', error);
    return false;
  }
}

OAuth 2.0 / Social Login

npm install @react-native-google-signin/google-signin
import {GoogleSignin} from '@react-native-google-signin/google-signin';

GoogleSignin.configure({
  webClientId: 'YOUR_WEB_CLIENT_ID',
  offlineAccess: true,
});

async function signInWithGoogle() {
  try {
    await GoogleSignin.hasPlayServices();
    const userInfo = await GoogleSignin.signIn();
    
    // Send token to your backend
    const response = await fetch('https://api.example.com/auth/google', {
      method: 'POST',
      headers: {'Content-Type': 'application/json'},
      body: JSON.stringify({
        idToken: userInfo.idToken,
      }),
    });
    
    const data = await response.json();
    await storeAuthToken(data.token);
  } catch (error) {
    console.error('Google sign-in error:', error);
  }
}

Input Validation

Sanitize User Input

// ❌ Vulnerable to injection
function unsafeQuery(userInput) {
  return `SELECT * FROM users WHERE username = '${userInput}'`;
}

// ✅ Use parameterized queries on backend
function safeQuery(userInput) {
  // Frontend: Validate and sanitize
  const sanitized = userInput.trim().replace(/[^a-zA-Z0-9]/g, '');
  
  // Backend: Use parameterized queries
  // SELECT * FROM users WHERE username = ?
  return sanitized;
}

Validate Forms

import {z} from 'zod';

const loginSchema = z.object({
  email: z.string().email('Invalid email address'),
  password: z.string().min(8, 'Password must be at least 8 characters'),
});

function validateLoginForm(data) {
  try {
    loginSchema.parse(data);
    return {valid: true};
  } catch (error) {
    return {
      valid: false,
      errors: error.errors,
    };
  }
}

// Usage
const result = validateLoginForm({
  email: userEmail,
  password: userPassword,
});

if (!result.valid) {
  console.error('Validation errors:', result.errors);
  return;
}

Prevent XSS

import {Text} from 'react-native';

// ✅ React Native Text is safe by default
function UserComment({comment}) {
  // This is automatically escaped
  return <Text>{comment}</Text>;
}

// ❌ Be careful with HTML rendering libraries
import HTML from 'react-native-render-html';

function UnsafeHTML({html}) {
  // Sanitize HTML before rendering
  const sanitized = sanitizeHtml(html, {
    allowedTags: ['p', 'br', 'strong', 'em'],
    allowedAttributes: {},
  });
  
  return <HTML source={{html: sanitized}} />;
}

Code Obfuscation

JavaScript Obfuscation

npm install --save-dev react-native-obfuscating-transformer
metro.config.js:
const obfuscatingTransformer = require('react-native-obfuscating-transformer');

module.exports = {
  transformer: {
    babelTransformerPath: obfuscatingTransformer,
  },
};
Obfuscation makes code harder to read but doesn’t make it completely secure. Never rely solely on obfuscation.

ProGuard (Android)

android/app/build.gradle:
android {
    buildTypes {
        release {
            minifyEnabled true
            shrinkResources true
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}
android/app/proguard-rules.pro:
# Keep React Native classes
-keep class com.facebook.react.** { *; }
-keep class com.facebook.hermes.** { *; }

# Keep your app classes
-keep class com.yourapp.** { *; }
import {Linking} from 'react-native';

const ALLOWED_HOSTS = ['myapp.com', 'www.myapp.com'];

Linking.addEventListener('url', ({url}) => {
  try {
    const parsed = new URL(url);
    
    // Validate host
    if (!ALLOWED_HOSTS.includes(parsed.hostname)) {
      console.warn('Invalid deep link host:', parsed.hostname);
      return;
    }
    
    // Handle valid deep link
    handleDeepLink(parsed);
  } catch (error) {
    console.error('Invalid URL:', error);
  }
});
Use universal/app links instead of custom URL schemes for better security. iOS ios/MyApp/MyApp.entitlements:
<key>com.apple.developer.associated-domains</key>
<array>
    <string>applinks:myapp.com</string>
</array>
Android android/app/src/main/AndroidManifest.xml:
<intent-filter android:autoVerify="true">
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" />
    <data android:scheme="https" android:host="myapp.com" />
</intent-filter>

Permissions

Request Only Necessary Permissions

import {PermissionsAndroid, Platform} from 'react-native';

async function requestCameraPermission() {
  if (Platform.OS === 'android') {
    const granted = await PermissionsAndroid.request(
      PermissionsAndroid.PERMISSIONS.CAMERA,
      {
        title: 'Camera Permission',
        message: 'App needs camera access to take photos',
        buttonNeutral: 'Ask Me Later',
        buttonNegative: 'Cancel',
        buttonPositive: 'OK',
      },
    );
    return granted === PermissionsAndroid.RESULTS.GRANTED;
  }
  return true; // iOS permissions handled in Info.plist
}
iOS ios/MyApp/Info.plist:
<key>NSCameraUsageDescription</key>
<string>We need camera access to take photos for your profile</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need photo library access to select images</string>

Jailbreak/Root Detection

npm install jail-monkey
import JailMonkey from 'jail-monkey';

function checkDeviceSecurity() {
  const isJailBroken = JailMonkey.isJailBroken();
  const canMockLocation = JailMonkey.canMockLocation();
  const isOnExternalStorage = JailMonkey.isOnExternalStorage();
  const isDebuggedMode = JailMonkey.isDebuggedMode();
  
  if (isJailBroken) {
    console.warn('Device is jailbroken/rooted');
    // Handle accordingly - show warning or disable sensitive features
  }
  
  return {
    isJailBroken,
    canMockLocation,
    isOnExternalStorage,
    isDebuggedMode,
  };
}
Jailbreak/root detection can be bypassed. Use it as one layer of defense, not the only security measure.

Secure Coding Practices

Avoid Console Logs in Production

// Use conditional logging
const isDev = __DEV__;

const logger = {
  log: (...args) => isDev && console.log(...args),
  warn: (...args) => isDev && console.warn(...args),
  error: (...args) => console.error(...args), // Always log errors
};

// Usage
logger.log('Debug info'); // Only in development
logger.error('Critical error'); // Always logged

Remove Debug Code

// ❌ Remove before production
if (__DEV__) {
  console.log('API Response:', data);
  console.log('User token:', token);
}

// ✅ Use proper logging service
import analytics from '@react-native-firebase/analytics';

analytics().logEvent('api_call', {
  endpoint: '/user/profile',
  success: true,
});

Secure Random Number Generation

// ❌ Insecure
const insecureRandom = Math.random();

// ✅ Cryptographically secure
import {randomBytes} from 'react-native-randombytes';

randomBytes(32, (error, bytes) => {
  if (!error) {
    const secureRandom = bytes.toString('hex');
    console.log(secureRandom);
  }
});

Security Checklist

  • Use EncryptedStorage for sensitive data
  • Use Keychain/Keystore for credentials
  • Never store passwords in plain text
  • Clear sensitive data on logout
  • Implement secure data migration
  • Use HTTPS for all API calls
  • Implement certificate pinning
  • Store API keys securely
  • Implement token refresh
  • Validate SSL certificates
  • Handle network errors securely
  • Implement secure login
  • Use biometric authentication
  • Implement session management
  • Handle token expiration
  • Implement secure logout
  • Use OAuth for social login
  • Obfuscate production code
  • Enable ProGuard (Android)
  • Remove debug code
  • Validate all user input
  • Implement error handling
  • Use TypeScript for type safety
  • Implement jailbreak detection
  • Request minimal permissions
  • Validate deep links
  • Implement rate limiting
  • Use secure random generation
  • Regular security audits

Testing Security

Automated Security Testing

# Dependency vulnerability scanning
npm audit
npm audit fix

# Or use yarn
yarn audit

Manual Testing

  1. Test with HTTP Proxy: Burp Suite, Charles Proxy
  2. Test on jailbroken/rooted devices
  3. Test with mock data
  4. Test error scenarios
  5. Test session timeout

Security Resources

Next Steps

Publishing

Publish your secure app to stores

Troubleshooting

Debug security-related issues

Build docs developers (and LLMs) love