# Biometric authentication uses LocalAuthentication API

- **Pattern:** `ab-001908` (`mobile-permissions-privacy.data-handling.biometric-auth-api`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001908
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001908)

## Why it matters

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.

## Severity rationale

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.

## Remediation

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.

```typescript
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.

## Detection

- **ID:** `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:

  ```typescript
  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:

  ```typescript
  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
    }
  }
  ```

## External references

- cwe CWE-287
- owasp A07
- external apple-local-authentication — https://developer.apple.com/documentation/localauthentication/logging_a_user_into_your_app_with_face_id_or_touch_id
- gdpr Art. 9

Taxons: access-control

HTML version: https://auditbuffet.com/patterns/ab-001908
