expo-local-authentication
Version: 55.0.6
Provides an API for Face ID and Touch ID (iOS) or the Fingerprint API (Android) to authenticate the user with a face or fingerprint scan.
Installation
npx expo install expo-local-authentication
Usage
import * as LocalAuthentication from 'expo-local-authentication';
import { Button, Text, View } from 'react-native';
export default function App() {
const authenticate = async () => {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
if (!hasHardware) {
alert('Biometric hardware not available');
return;
}
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!isEnrolled) {
alert('No biometric records found');
return;
}
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to continue',
});
if (result.success) {
alert('Authentication successful!');
} else {
alert('Authentication failed');
}
};
return (
<View>
<Button title="Authenticate" onPress={authenticate} />
</View>
);
}
API Reference
Methods
Checks if device has biometric authentication hardwareconst hasHardware = await LocalAuthentication.hasHardwareAsync();
if (hasHardware) {
// Device supports biometric authentication
}
Checks if user has enrolled biometric recordsconst isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!isEnrolled) {
alert('Please enroll biometric authentication in settings');
}
authenticateAsync(options)
(options?: LocalAuthenticationOptions) => Promise<LocalAuthenticationResult>
Authenticates user with biometricsOptions:
promptMessage: Message shown in authentication dialog
fallbackLabel: iOS fallback button text
cancelLabel: Android cancel button text
disableDeviceFallback: Disable device PIN fallback
requireConfirmation: Require explicit user confirmation (Android)
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Unlock to continue',
fallbackLabel: 'Use passcode',
disableDeviceFallback: false,
});
if (result.success) {
console.log('Authenticated');
} else {
console.log('Error:', result.error);
}
supportedAuthenticationTypesAsync()
() => Promise<AuthenticationType[]>
Gets supported authentication typesReturns array of: FINGERPRINT, FACIAL_RECOGNITION, IRISconst types = await LocalAuthentication.supportedAuthenticationTypesAsync();
if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
console.log('Face ID/Face Recognition available');
}
getEnrolledLevelAsync()
() => Promise<SecurityLevel>
Gets security level of enrolled authenticationReturns: NONE, SECRET, BIOMETRICconst level = await LocalAuthentication.getEnrolledLevelAsync();
console.log('Security level:', level);
Cancels authentication prompt (Android only)LocalAuthentication.cancelAuthenticate();
Types
LocalAuthenticationResult
Whether authentication was successful
Error message if authentication failed
LocalAuthenticationOptions
Message displayed in authentication dialog
iOS: Text for fallback button
Android: Text for cancel button
Disable device PIN/pattern fallback. Default: false
Android: Require explicit confirmation after biometric scan. Default: true
Enums
enum AuthenticationType {
FINGERPRINT = 1,
FACIAL_RECOGNITION = 2,
IRIS = 3,
}
enum SecurityLevel {
NONE = 0,
SECRET = 1,
BIOMETRIC = 2,
}
Examples
Basic Authentication
import * as LocalAuthentication from 'expo-local-authentication';
async function authenticateUser() {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to access your data',
});
if (result.success) {
console.log('Authentication successful');
return true;
} else {
console.log('Authentication failed:', result.error);
return false;
}
}
Check Capability
import * as LocalAuthentication from 'expo-local-authentication';
async function checkBiometricCapability() {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
if (!hasHardware) {
return {
available: false,
reason: 'Device does not have biometric hardware',
};
}
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
if (!isEnrolled) {
return {
available: false,
reason: 'No biometric records enrolled',
};
}
const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
return {
available: true,
types,
};
}
Determine Authentication Type
import * as LocalAuthentication from 'expo-local-authentication';
async function getBiometricType() {
const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
return 'Face ID';
} else if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
return 'Touch ID';
} else if (types.includes(LocalAuthentication.AuthenticationType.IRIS)) {
return 'Iris Recognition';
}
return 'Unknown';
}
// Use in prompt
const biometricType = await getBiometricType();
await LocalAuthentication.authenticateAsync({
promptMessage: `Use ${biometricType} to unlock`,
});
Protect Sensitive Action
import * as LocalAuthentication from 'expo-local-authentication';
async function deleteSensitiveData() {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to delete data',
fallbackLabel: 'Use passcode instead',
});
if (!result.success) {
alert('Authentication required');
return;
}
// Proceed with deletion
await performDelete();
}
With SecureStore Integration
import * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
async function saveSecureData(key: string, value: string) {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to save secure data',
});
if (!result.success) {
throw new Error('Authentication required');
}
await SecureStore.setItemAsync(key, value, {
requireAuthentication: true,
authenticationPrompt: 'Authenticate to access',
});
}
async function loadSecureData(key: string) {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to view data',
});
if (!result.success) {
return null;
}
return await SecureStore.getItemAsync(key);
}
Complete Example
import * as LocalAuthentication from 'expo-local-authentication';
import { useState, useEffect } from 'react';
import { View, Button, Text, StyleSheet, Alert } from 'react-native';
export default function BiometricAuthExample() {
const [isAvailable, setIsAvailable] = useState(false);
const [biometricType, setBiometricType] = useState<string>('');
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
checkBiometrics();
}, []);
async function checkBiometrics() {
const hasHardware = await LocalAuthentication.hasHardwareAsync();
const isEnrolled = await LocalAuthentication.isEnrolledAsync();
setIsAvailable(hasHardware && isEnrolled);
if (hasHardware && isEnrolled) {
const types = await LocalAuthentication.supportedAuthenticationTypesAsync();
if (types.includes(LocalAuthentication.AuthenticationType.FACIAL_RECOGNITION)) {
setBiometricType('Face ID');
} else if (types.includes(LocalAuthentication.AuthenticationType.FINGERPRINT)) {
setBiometricType('Touch ID / Fingerprint');
} else {
setBiometricType('Biometric');
}
}
}
async function handleAuthentication() {
try {
const result = await LocalAuthentication.authenticateAsync({
promptMessage: `Authenticate with ${biometricType}`,
fallbackLabel: 'Use passcode',
});
if (result.success) {
setIsAuthenticated(true);
Alert.alert('Success', 'Authentication successful!');
} else {
Alert.alert('Failed', result.error || 'Authentication failed');
}
} catch (error) {
Alert.alert('Error', 'An error occurred');
}
}
function handleLogout() {
setIsAuthenticated(false);
}
if (!isAvailable) {
return (
<View style={styles.container}>
<Text style={styles.message}>
Biometric authentication not available
</Text>
<Text style={styles.subtitle}>
Please enable Face ID or Touch ID in your device settings
</Text>
</View>
);
}
if (isAuthenticated) {
return (
<View style={styles.container}>
<Text style={styles.success}>✓ Authenticated</Text>
<Text style={styles.message}>You have access to sensitive data</Text>
<Button title="Logout" onPress={handleLogout} />
</View>
);
}
return (
<View style={styles.container}>
<Text style={styles.title}>Biometric Authentication</Text>
<Text style={styles.subtitle}>
Use {biometricType} to authenticate
</Text>
<Button title="Authenticate" onPress={handleAuthentication} />
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 20,
gap: 15,
},
title: {
fontSize: 24,
fontWeight: 'bold',
},
subtitle: {
fontSize: 16,
color: '#666',
textAlign: 'center',
},
message: {
fontSize: 18,
textAlign: 'center',
},
success: {
fontSize: 48,
color: '#4CAF50',
},
});
| Platform | Supported | Methods |
|---|
| iOS | ✅ | Face ID, Touch ID |
| Android | ✅ | Fingerprint, Face unlock |
| Web | ❌ | Not available |
Permissions
iOS
Add to app.json:
{
"expo": {
"plugins": [
[
"expo-local-authentication",
{
"faceIDPermission": "Allow $(PRODUCT_NAME) to use Face ID."
}
]
]
}
}
Android
Permissions automatically added:
USE_BIOMETRIC (Android 9+)
USE_FINGERPRINT (Android 6-8)
Best Practices
- Check Availability: Always check hardware and enrollment before authenticating
- Graceful Fallback: Provide alternative authentication if biometrics fail
- Clear Prompts: Use descriptive
promptMessage explaining why auth is needed
- Error Handling: Handle all authentication errors gracefully
- User Choice: Don’t force biometric auth; offer alternatives
Always check both hasHardwareAsync() and isEnrolledAsync() before attempting authentication.
Common Error Codes
user_cancel: User canceled authentication
system_cancel: System canceled (e.g., app backgrounded)
authentication_failed: Biometric scan failed
passcode_not_set: Device passcode not set
not_available: Biometric authentication not available
Resources