Biometric data (Face ID scans, fingerprint templates) is classified as sensitive biometric data under GDPR Art. 9 and is subject to heightened processing restrictions. The platform's official LocalAuthentication API (iOS) and BiometricPrompt (Android) are specifically designed so that biometric data never leaves the secure enclave — they return only a boolean pass/fail. Custom biometric implementations that attempt to capture or process biometric data directly violate GDPR Art. 9 and OWASP A07 (Identification and Authentication Failures). CWE-287 (Improper Authentication) applies when biometric auth lacks proper fallback to passcode, leaving users locked out on unenrolled devices. An implementation without hardware-availability and enrollment checks crashes on devices without biometric sensors.
Medium because a missing LocalAuthentication API creates crash risks on devices without biometric sensors and, for custom implementations, potential GDPR Art. 9 violations around biometric data processing.
Use expo-local-authentication (Expo) or react-native-biometrics (bare React Native). Always check hardware availability and enrollment before calling authenticate, and always enable device passcode as fallback.
import * as LocalAuthentication from 'expo-local-authentication'
async function authenticateUser(): Promise<boolean> {
if (!await LocalAuthentication.hasHardwareAsync()) {
// Fall through to passcode/password auth
return false
}
if (!await LocalAuthentication.isEnrolledAsync()) {
Alert.alert('No Biometrics', 'No biometric data enrolled. Use your passcode.')
return false
}
const { success } = await LocalAuthentication.authenticateAsync({
promptMessage: 'Authenticate to continue',
fallbackLabel: 'Use passcode',
disableDeviceFallback: false, // Always allow passcode fallback
})
return success
}
Never attempt to capture, store, or transmit biometric data directly. The platform APIs are the only compliant path — they ensure biometric templates never leave the secure enclave.
ID: mobile-permissions-privacy.data-handling.biometric-auth-api
Severity: medium
What to look for: Search for biometric authentication code (Face ID, Touch ID, fingerprint). Quote the actual import used (expo-local-authentication, react-native-biometrics, or custom implementation). Enumerate all biometric call sites and check for at least 3 required handling paths: hardware availability check, enrollment check, and authentication result handling.
Pass criteria: Biometric authentication (if implemented) uses the platform's official LocalAuthentication API (expo-local-authentication or react-native-biometrics). Implementation includes at least 3 checks: hardware availability, biometric enrollment status, and authentication result handling with fallback to passcode.
Fail criteria: Custom biometric implementation found. App attempts to access biometric sensors directly instead of using the official API. Or biometric authentication lacks error handling or fallback (fewer than 3 handling paths).
Skip (N/A) when: The app does not implement biometric authentication (no biometric-related imports or code found).
Detail on fail: Quote the actual import and implementation. "App uses custom Face ID detection instead of official LocalAuthentication API" or "Biometric auth crashes if device has no fingerprint enrolled instead of falling back to passcode."
Remediation: Use the official LocalAuthentication API for biometric auth. For Expo:
import * as LocalAuthentication from 'expo-local-authentication'
async function authenticateWithBiometric() {
try {
const compatible = await LocalAuthentication.hasHardwareAsync()
if (!compatible) {
throw new Error('Device does not support biometric authentication')
}
const enrolled = await LocalAuthentication.isEnrolledAsync()
if (!enrolled) {
throw new Error('No biometric data enrolled on device')
}
const result = await LocalAuthentication.authenticateAsync({
reason: 'Authenticate to access your account',
fallbackLabel: 'Use passcode',
disableDeviceFallback: false,
})
if (result.success) {
// User authenticated
return true
} else {
// User cancelled or failed
return false
}
} catch (error) {
console.error('Biometric auth error:', error.message)
// Fall back to password/passcode
return false
}
}
For bare React Native:
import ReactNativeBiometrics from 'react-native-biometrics'
async function authenticateWithBiometric() {
try {
const { available, biometryType } = await ReactNativeBiometrics.isSensorAvailable()
if (!available) {
throw new Error('Biometric sensor not available')
}
const { success } = await ReactNativeBiometrics.simplePrompt({
promptMessage: 'Authenticate to access your account',
fallbackPromptMessage: 'Use your passcode',
})
return success
} catch (error) {
console.error('Biometric auth error:', error)
return false
}
}