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 );
}
}
// ❌ 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 ;
}
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.** { *; }
Deep Link Security
Validate Deep Links
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 );
}
});
Universal Links (iOS) / App Links (Android)
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
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
Testing Security
Automated Security Testing
# Dependency vulnerability scanning
npm audit
npm audit fix
# Or use yarn
yarn audit
Manual Testing
Test with HTTP Proxy : Burp Suite, Charles Proxy
Test on jailbroken/rooted devices
Test with mock data
Test error scenarios
Test session timeout
Security Resources
Next Steps
Publishing Publish your secure app to stores
Troubleshooting Debug security-related issues