Building a React Native App with Biometric Authentication (Face ID / Touch ID)
Introduction
Biometric authentication has become a standard feature in modern mobile applications, providing users with a secure and convenient way to access their accounts without remembering passwords. In this comprehensive guide, we'll walk through implementing Face ID and Touch ID authentication in a React Native application for both iOS and Android platforms.
By the end of this tutorial, you'll have a fully functional biometric authentication system that enhances your app's security while improving user experience.
What is Biometric Authentication?
Biometric authentication is a security process that uses unique biological characteristics to verify a user's identity. In the context of mobile devices, this typically includes:
- Face ID: Uses facial recognition technology (available on iPhone X and later)
- Touch ID: Uses fingerprint recognition (available on devices with fingerprint sensors)
- Android Fingerprint: Android's fingerprint authentication system
- Android Face Unlock: Android's facial recognition system
These authentication methods provide a more secure and user-friendly alternative to traditional password-based systems.
Why Use Biometric Authentication?
Implementing biometric authentication in your React Native app offers several compelling benefits:
- Enhanced Security: Biometric data is unique to each individual and difficult to replicate, making it more secure than traditional passwords
- Improved User Experience: Users can authenticate quickly without typing passwords, reducing friction in the login process
- Reduced Password Fatigue: Eliminates the need for users to remember and manage multiple complex passwords
- Industry Standard: Users expect biometric authentication as a standard feature in financial, healthcare, and other sensitive applications
- Compliance: Helps meet security requirements for regulated industries
- Multi-Factor Authentication: Can be combined with other authentication methods for additional security
Prerequisites
Before we begin, make sure you have:
- React Native development environment set up (React Native 0.60 or higher)
- Xcode installed (for iOS development)
- Android Studio installed (for Android development)
- A physical device with biometric capabilities (simulators have limited biometric testing)
- Basic understanding of React Native and JavaScript/TypeScript
Installing Required Packages
We'll use the react-native-biometrics
library, which provides a unified API for both iOS and Android biometric authentication. This library is actively maintained and supports both the old and new React Native architectures.
First, install the package:
npm install react-native-biometrics
# or
yarn add react-native-biometrics
For iOS, install the CocoaPods dependencies:
cd ios && pod install && cd ..
iOS Configuration
To enable biometric authentication on iOS, you need to add a privacy description to your Info.plist
file. This tells users why your app needs access to Face ID.
Open ios/YourAppName/Info.plist
and add:
<key>NSFaceIDUsageDescription</key>
<string>We use Face ID to securely authenticate you and protect your account</string>
This message will be shown to users when they're first prompted to use Face ID.
Android Configuration
For Android, you need to add permissions to your AndroidManifest.xml
file:
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<uses-permission android:name="android.permission.USE_FINGERPRINT" />
Open android/app/src/main/AndroidManifest.xml
and add these permissions inside the <manifest>
tag.
Creating a Biometric Authentication Service
Let's create a reusable service that handles all biometric authentication logic. Create a new file services/BiometricAuth.ts
:
import ReactNativeBiometrics, { BiometryTypes } from 'react-native-biometrics';
class BiometricAuthService {
private rnBiometrics: ReactNativeBiometrics;
constructor() {
this.rnBiometrics = new ReactNativeBiometrics({
allowDeviceCredentials: true,
});
}
/**
* Check if biometric authentication is available on the device
*/
async isBiometricAvailable(): Promise<{
available: boolean;
biometryType: string | null;
}> {
try {
const { available, biometryType } = await this.rnBiometrics.isSensorAvailable();
let biometricType = null;
if (biometryType === BiometryTypes.TouchID) {
biometricType = 'Touch ID';
} else if (biometryType === BiometryTypes.FaceID) {
biometricType = 'Face ID';
} else if (biometryType === BiometryTypes.Biometrics) {
biometricType = 'Biometrics';
}
return {
available,
biometryType: biometricType,
};
} catch (error) {
console.error('Error checking biometric availability:', error);
return {
available: false,
biometryType: null,
};
}
}
/**
* Create biometric signature keys (for cryptographic operations)
*/
async createKeys(): Promise<{ publicKey: string } | null> {
try {
const { publicKey } = await this.rnBiometrics.createKeys();
return { publicKey };
} catch (error) {
console.error('Error creating biometric keys:', error);
return null;
}
}
/**
* Delete biometric keys
*/
async deleteKeys(): Promise<boolean> {
try {
const { keysDeleted } = await this.rnBiometrics.deleteKeys();
return keysDeleted;
} catch (error) {
console.error('Error deleting biometric keys:', error);
return false;
}
}
/**
* Check if biometric keys exist
*/
async biometricKeysExist(): Promise<boolean> {
try {
const { keysExist } = await this.rnBiometrics.biometricKeysExist();
return keysExist;
} catch (error) {
console.error('Error checking if biometric keys exist:', error);
return false;
}
}
/**
* Authenticate user with biometrics (simple prompt)
*/
async simplePrompt(promptMessage: string): Promise<{
success: boolean;
error?: string;
}> {
try {
const { success } = await this.rnBiometrics.simplePrompt({
promptMessage,
cancelButtonText: 'Cancel',
});
return { success };
} catch (error: any) {
console.error('Biometric authentication error:', error);
return {
success: false,
error: error.message || 'Authentication failed',
};
}
}
/**
* Authenticate with signature (more secure, uses cryptographic operations)
*/
async createSignature(
promptMessage: string,
payload: string
): Promise<{
success: boolean;
signature?: string;
error?: string;
}> {
try {
const { success, signature } = await this.rnBiometrics.createSignature({
promptMessage,
payload,
cancelButtonText: 'Cancel',
});
if (success && signature) {
return { success: true, signature };
}
return { success: false, error: 'Failed to create signature' };
} catch (error: any) {
console.error('Error creating signature:', error);
return {
success: false,
error: error.message || 'Signature creation failed',
};
}
}
}
export default new BiometricAuthService();
Building the Authentication UI
Now let's create a React component that uses our biometric service. Create screens/BiometricLoginScreen.tsx
:
import React, { useState, useEffect } from 'react';
import {
View,
Text,
TouchableOpacity,
StyleSheet,
Alert,
ActivityIndicator,
} from 'react-native';
import BiometricAuth from '../services/BiometricAuth';
const BiometricLoginScreen: React.FC = () => {
const [biometricType, setBiometricType] = useState<string | null>(null);
const [isAvailable, setIsAvailable] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
useEffect(() => {
checkBiometricAvailability();
}, []);
const checkBiometricAvailability = async () => {
setLoading(true);
const { available, biometryType } = await BiometricAuth.isBiometricAvailable();
setIsAvailable(available);
setBiometricType(biometryType);
setLoading(false);
if (!available) {
Alert.alert(
'Biometric Authentication Not Available',
'Your device does not support biometric authentication or it is not configured.',
[{ text: 'OK' }]
);
}
};
const handleSimpleAuthentication = async () => {
if (!isAvailable) {
Alert.alert('Error', 'Biometric authentication is not available');
return;
}
const promptMessage = `Authenticate with ${biometricType || 'Biometrics'}`;
const { success, error } = await BiometricAuth.simplePrompt(promptMessage);
if (success) {
setIsAuthenticated(true);
Alert.alert('Success', 'Authentication successful!');
} else {
Alert.alert('Authentication Failed', error || 'Please try again');
}
};
const handleSecureAuthentication = async () => {
if (!isAvailable) {
Alert.alert('Error', 'Biometric authentication is not available');
return;
}
// Check if keys exist, create them if not
const keysExist = await BiometricAuth.biometricKeysExist();
if (!keysExist) {
const result = await BiometricAuth.createKeys();
if (!result) {
Alert.alert('Error', 'Failed to create biometric keys');
return;
}
}
// Create a unique payload (in production, this might be a challenge from your server)
const payload = `${Date.now()}-${Math.random()}`;
const promptMessage = `Authenticate with ${biometricType || 'Biometrics'}`;
const { success, signature, error } = await BiometricAuth.createSignature(
promptMessage,
payload
);
if (success && signature) {
setIsAuthenticated(true);
// In production, you would send this signature to your server for verification
console.log('Signature:', signature);
Alert.alert('Success', 'Secure authentication successful!');
} else {
Alert.alert('Authentication Failed', error || 'Please try again');
}
};
const handleLogout = () => {
setIsAuthenticated(false);
Alert.alert('Logged Out', 'You have been logged out successfully');
};
if (loading) {
return (
<View style={styles.container}>
<ActivityIndicator size="large" color="#007AFF" />
<Text style={styles.loadingText}>Checking biometric availability...</Text>
</View>
);
}
if (isAuthenticated) {
return (
<View style={styles.container}>
<View style={styles.successContainer}>
<Text style={styles.successIcon}>✓</Text>
<Text style={styles.successTitle}>Authentication Successful</Text>
<Text style={styles.successMessage}>
You have been authenticated using {biometricType}
</Text>
<TouchableOpacity style={styles.logoutButton} onPress={handleLogout}>
<Text style={styles.logoutButtonText}>Logout</Text>
</TouchableOpacity>
</View>
</View>
);
}
return (
<View style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Biometric Authentication</Text>
<Text style={styles.subtitle}>
{isAvailable
? `${biometricType} is available on this device`
: 'Biometric authentication is not available'}
</Text>
</View>
{isAvailable && (
<View style={styles.buttonContainer}>
<TouchableOpacity
style={styles.button}
onPress={handleSimpleAuthentication}
>
<Text style={styles.buttonText}>Simple Authentication</Text>
<Text style={styles.buttonDescription}>
Quick authentication without cryptographic keys
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.button, styles.secureButton]}
onPress={handleSecureAuthentication}
>
<Text style={styles.buttonText}>Secure Authentication</Text>
<Text style={styles.buttonDescription}>
Uses cryptographic signatures for enhanced security
</Text>
</TouchableOpacity>
</View>
)}
<View style={styles.infoContainer}>
<Text style={styles.infoTitle}>How it works:</Text>
<Text style={styles.infoText}>
• Simple Authentication: Uses the device's biometric sensor for quick verification
</Text>
<Text style={styles.infoText}>
• Secure Authentication: Creates a cryptographic signature that can be verified by your backend
</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#F5F5F5',
padding: 20,
justifyContent: 'center',
},
header: {
marginBottom: 40,
alignItems: 'center',
},
title: {
fontSize: 28,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
subtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
},
buttonContainer: {
marginBottom: 30,
},
button: {
backgroundColor: '#007AFF',
padding: 20,
borderRadius: 12,
marginBottom: 15,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 4,
elevation: 3,
},
secureButton: {
backgroundColor: '#34C759',
},
buttonText: {
color: '#FFFFFF',
fontSize: 18,
fontWeight: '600',
marginBottom: 5,
},
buttonDescription: {
color: '#FFFFFF',
fontSize: 13,
opacity: 0.8,
},
infoContainer: {
backgroundColor: '#FFFFFF',
padding: 20,
borderRadius: 12,
marginTop: 20,
},
infoTitle: {
fontSize: 16,
fontWeight: '600',
color: '#333',
marginBottom: 10,
},
infoText: {
fontSize: 14,
color: '#666',
marginBottom: 8,
lineHeight: 20,
},
successContainer: {
alignItems: 'center',
},
successIcon: {
fontSize: 80,
color: '#34C759',
marginBottom: 20,
},
successTitle: {
fontSize: 24,
fontWeight: 'bold',
color: '#333',
marginBottom: 10,
},
successMessage: {
fontSize: 16,
color: '#666',
textAlign: 'center',
marginBottom: 30,
},
logoutButton: {
backgroundColor: '#FF3B30',
paddingHorizontal: 40,
paddingVertical: 15,
borderRadius: 12,
},
logoutButtonText: {
color: '#FFFFFF',
fontSize: 16,
fontWeight: '600',
},
loadingText: {
marginTop: 15,
fontSize: 16,
color: '#666',
},
});
export default BiometricLoginScreen;
Integrating with Your Backend
For production applications, you should integrate biometric authentication with your backend for enhanced security. Here's a typical flow:
Authentication Flow with Backend
// services/AuthAPI.ts
import BiometricAuth from './BiometricAuth';
class AuthAPI {
private baseURL = 'https://your-api.com';
/**
* Step 1: Get authentication challenge from server
*/
async getAuthChallenge(userId: string): Promise<string | null> {
try {
const response = await fetch(`${this.baseURL}/auth/challenge`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ userId }),
});
const data = await response.json();
return data.challenge;
} catch (error) {
console.error('Error getting auth challenge:', error);
return null;
}
}
/**
* Step 2: Verify biometric signature with server
*/
async verifyBiometricSignature(
userId: string,
signature: string,
challenge: string
): Promise<{ success: boolean; token?: string }> {
try {
const response = await fetch(`${this.baseURL}/auth/verify-biometric`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
userId,
signature,
challenge,
}),
});
const data = await response.json();
if (data.success) {
return {
success: true,
token: data.token,
};
}
return { success: false };
} catch (error) {
console.error('Error verifying biometric signature:', error);
return { success: false };
}
}
/**
* Complete biometric login flow
*/
async loginWithBiometrics(userId: string): Promise<{
success: boolean;
token?: string;
error?: string;
}> {
// Step 1: Get challenge from server
const challenge = await this.getAuthChallenge(userId);
if (!challenge) {
return {
success: false,
error: 'Failed to get authentication challenge',
};
}
// Step 2: Create biometric signature
const { success, signature, error } = await BiometricAuth.createSignature(
'Authenticate to continue',
challenge
);
if (!success || !signature) {
return {
success: false,
error: error || 'Biometric authentication failed',
};
}
// Step 3: Verify signature with server
const result = await this.verifyBiometricSignature(userId, signature, challenge);
return result;
}
}
export default new AuthAPI();
Handling Edge Cases and Errors
Biometric authentication can fail for various reasons. Here's how to handle common scenarios:
import { Alert, Platform } from 'react-native';
import BiometricAuth from '../services/BiometricAuth';
const handleBiometricAuth = async () => {
try {
// Check availability first
const { available, biometryType } = await BiometricAuth.isBiometricAvailable();
if (!available) {
Alert.alert(
'Biometric Not Available',
'Please enable biometric authentication in your device settings or use password login.',
[
{ text: 'Use Password', onPress: () => showPasswordLogin() },
{ text: 'OK' },
]
);
return;
}
// Attempt authentication
const { success, error } = await BiometricAuth.simplePrompt(
`Authenticate with ${biometryType}`
);
if (success) {
// Authentication successful
proceedToApp();
} else {
// Handle specific error cases
if (error?.includes('cancelled') || error?.includes('canceled')) {
// User cancelled - do nothing or show gentle reminder
return;
}
if (error?.includes('lockout') || error?.includes('too many attempts')) {
Alert.alert(
'Too Many Attempts',
'Biometric authentication is temporarily locked. Please try again later or use password login.',
[
{ text: 'Use Password', onPress: () => showPasswordLogin() },
{ text: 'OK' },
]
);
return;
}
if (error?.includes('not enrolled') || error?.includes('no biometrics')) {
Alert.alert(
'Setup Required',
`Please set up ${biometryType} in your device settings to use this feature.`,
[
{ text: 'Use Password', onPress: () => showPasswordLogin() },
{ text: 'OK' },
]
);
return;
}
// Generic error
Alert.alert(
'Authentication Failed',
'Could not authenticate. Please try again or use password login.',
[
{ text: 'Try Again', onPress: () => handleBiometricAuth() },
{ text: 'Use Password', onPress: () => showPasswordLogin() },
]
);
}
} catch (error) {
console.error('Unexpected error during biometric auth:', error);
Alert.alert(
'Error',
'An unexpected error occurred. Please use password login.',
[{ text: 'OK', onPress: () => showPasswordLogin() }]
);
}
};
const proceedToApp = () => {
// Navigate to main app screen
};
const showPasswordLogin = () => {
// Show password login screen
};
Testing Biometric Authentication
iOS Testing
- Physical Device: The best way to test is on a physical device with Face ID or Touch ID
- Simulator: You can test on the simulator, but with limitations:
- Face ID: Hardware > Face ID > Enrolled
- Face ID Match: Hardware > Face ID > Matching Face
- Face ID Non-Match: Hardware > Face ID > Non-matching Face
Android Testing
- Physical Device: Test on a device with fingerprint or face unlock
- Emulator:
- Enable fingerprint in AVD settings
- Use
adb
command to simulate fingerprint:adb -e emu finger touch 1
Testing Checklist
- ✅ Test with biometrics enabled
- ✅ Test with biometrics disabled
- ✅ Test with no biometrics enrolled
- ✅ Test cancellation flow
- ✅ Test multiple failed attempts
- ✅ Test after device restart
- ✅ Test with app in background
- ✅ Test on different device models
Best Practices
Security Best Practices
- Never store sensitive data based solely on biometric authentication: Always validate with your backend
- Use fallback authentication: Provide password/PIN option when biometrics fail
- Implement rate limiting: Prevent brute force attempts on your backend
- Use cryptographic signatures: For sensitive operations, use the signature-based authentication
- Validate on the server: Never trust client-side authentication alone
- Handle biometric changes: Detect when users change their biometric data and require re-authentication
User Experience Best Practices
- Make it optional: Don't force users to use biometric authentication
- Provide clear messaging: Explain why you need biometric access
- Handle failures gracefully: Provide alternative authentication methods
- Test on real devices: Simulator testing is not sufficient
- Support re-enrollment: Allow users to re-enable biometrics after changes
- Show appropriate icons: Use fingerprint or face icons based on available biometric type
Code Best Practices
// Store biometric preference
import AsyncStorage from '@react-native-async-storage/async-storage';
const BIOMETRIC_ENABLED_KEY = '@biometric_enabled';
export const setBiometricEnabled = async (enabled: boolean): Promise<void> => {
try {
await AsyncStorage.setItem(BIOMETRIC_ENABLED_KEY, JSON.stringify(enabled));
} catch (error) {
console.error('Error saving biometric preference:', error);
}
};
export const isBiometricEnabled = async (): Promise<boolean> => {
try {
const value = await AsyncStorage.getItem(BIOMETRIC_ENABLED_KEY);
return value ? JSON.parse(value) : false;
} catch (error) {
console.error('Error reading biometric preference:', error);
return false;
}
};
// Use in settings screen
const BiometricSettings = () => {
const [biometricEnabled, setBiometricEnabledState] = useState(false);
useEffect(() => {
loadBiometricPreference();
}, []);
const loadBiometricPreference = async () => {
const enabled = await isBiometricEnabled();
setBiometricEnabledState(enabled);
};
const toggleBiometric = async (enabled: boolean) => {
if (enabled) {
// Test biometric first
const { success } = await BiometricAuth.simplePrompt('Verify to enable');
if (success) {
await setBiometricEnabled(true);
setBiometricEnabledState(true);
}
} else {
await setBiometricEnabled(false);
setBiometricEnabledState(false);
}
};
return (
<Switch value={biometricEnabled} onValueChange={toggleBiometric} />
);
};
Common Issues and Solutions
Issue 1: "Biometric authentication is not available"
Solution: Check device settings and ensure biometrics are enrolled:
const checkAndPromptBiometricSetup = async () => {
const { available } = await BiometricAuth.isBiometricAvailable();
if (!available) {
Alert.alert(
'Setup Biometric Authentication',
'Would you like to set up biometric authentication in your device settings?',
[
{
text: 'Go to Settings',
onPress: () => {
if (Platform.OS === 'ios') {
Linking.openURL('App-Prefs:');
} else {
Linking.openSettings();
}
},
},
{ text: 'Later', style: 'cancel' },
]
);
}
};
Issue 2: Keys not persisting after app restart
Solution: Keys should persist, but if they don't:
const ensureBiometricKeys = async () => {
const keysExist = await BiometricAuth.biometricKeysExist();
if (!keysExist) {
console.log('Keys do not exist, creating new keys...');
const result = await BiometricAuth.createKeys();
if (!result) {
console.error('Failed to create biometric keys');
return false;
}
}
return true;
};
Issue 3: Android authentication dialog not showing
Solution: Ensure proper permissions and FragmentActivity:
<!-- AndroidManifest.xml -->
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
<!-- Ensure your MainActivity extends FragmentActivity -->
Advanced: Implementing Auto-Fill Integration
For a more seamless experience, you can integrate with iOS AutoFill and Android Autofill:
// iOS AutoFill with Face ID
import { AccessControl, BiometryType } from 'react-native-biometrics';
const saveCredentialsWithBiometrics = async (username: string, password: string) => {
// This is a simplified example
// In production, use proper keychain/keystore libraries
const { success } = await BiometricAuth.simplePrompt(
'Authenticate to save credentials'
);
if (success) {
// Save to secure storage
// Implementation depends on your secure storage library
}
};
Performance Considerations
Biometric authentication is generally fast, but consider these optimizations:
- Cache availability check: Don't check on every render
- Lazy initialize: Only initialize biometric service when needed
- Timeout handling: Set appropriate timeouts for authentication prompts
- Background handling: Handle app state changes gracefully
import { AppState, AppStateStatus } from 'react-native';
const useBiometricAuth = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const appState = useRef(AppState.currentState);
useEffect(() => {
const subscription = AppState.addEventListener('change', handleAppStateChange);
return () => {
subscription.remove();
};
}, []);
const handleAppStateChange = (nextAppState: AppStateStatus) => {
if (
appState.current.match(/inactive|background/) &&
nextAppState === 'active'
) {
// App came to foreground - require re-authentication
setIsAuthenticated(false);
promptForBiometric();
}
appState.current = nextAppState;
};
const promptForBiometric = async () => {
const { success } = await BiometricAuth.simplePrompt('Authenticate to continue');
setIsAuthenticated(success);
};
return { isAuthenticated, promptForBiometric };
};
Conclusion
Implementing biometric authentication in React Native enhances both security and user experience. In this comprehensive guide, we've covered:
- Setting up react-native-biometrics for iOS and Android
- Creating a reusable biometric authentication service
- Building a complete authentication UI
- Integrating with backend services
- Handling edge cases and errors
- Testing strategies
- Best practices for security and UX
- Common issues and their solutions
Remember to always provide fallback authentication methods and test thoroughly on real devices. Biometric authentication should complement, not replace, other security measures in your application.
The complete code examples in this guide provide a solid foundation for implementing biometric authentication in your React Native apps. Adapt them to your specific requirements and always keep security best practices in mind.
Happy coding, and stay secure! 🔐