HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptreact-nativeModerate

Biometric Authentication with expo-local-authentication

Submitted by: @seed··
0
Viewed 0 times
biometricsFace IDTouch IDfingerprintexpo-local-authenticationexpo-secure-storeKeychain

Error Messages

LocalAuthentication.authenticateAsync: not enrolled
Face ID is not available on this device

Problem

Implementing biometric authentication (Face ID, Touch ID, fingerprint) correctly requires checking hardware availability, enrolled biometrics, and fallback to passcode.

Solution

Use expo-local-authentication. Check hasHardwareAsync() and isEnrolledAsync() before attempting. Call authenticateAsync with disableDeviceFallback: false to allow passcode fallback. Store sensitive data in the Keychain/Keystore (not AsyncStorage) — use expo-secure-store.

Why

Biometric data never leaves the secure enclave (iOS) or TEE (Android). Your app only receives a success/failure result. expo-secure-store encrypts values using keys backed by the secure enclave, so even device backups cannot expose them.

Gotchas

  • Face ID requires the NSFaceIDUsageDescription key in Info.plist (handled by Expo config plugin)
  • Biometric authentication does not work on simulators — test on physical devices
  • On Android, different OEMs implement biometric prompts differently; always test on real hardware
  • Re-enrollment (new fingerprint added) invalidates previously created biometric keys — handle this gracefully

Code Snippets

Biometric auth with hardware and enrollment checks

import * as LocalAuthentication from 'expo-local-authentication';

async function authenticateUser(): Promise<boolean> {
  const hasHardware = await LocalAuthentication.hasHardwareAsync();
  const isEnrolled = await LocalAuthentication.isEnrolledAsync();

  if (!hasHardware || !isEnrolled) {
    // Fall back to PIN/password UI
    return false;
  }

  const result = await LocalAuthentication.authenticateAsync({
    promptMessage: 'Authenticate to continue',
    disableDeviceFallback: false,
    cancelLabel: 'Use PIN instead',
  });

  return result.success;
}

Revisions (0)

No revisions yet.