How to Test Biometric Login on React Native (Complete Guide)
How to Test Biometric Login on React Native (Complete Guide)
How to Test Biometric Login on React Native (Complete Guide)
Biometric login has become a standard expectation for mobile users, offering a blend of convenience and security that passwords alone struggle to match. In a React Native app, the biometric flow touches native modules, bridges JavaScript‑to‑native calls, and relies on platform‑specific APIs such as LocalAuthentication on iOS and BiometricPrompt on Android. When any part of this chain misbehaves, users can be locked out, sensitive data can be exposed, or the app can crash silently. This guide walks you through a complete testing strategy—from why biometric login matters, through a detailed test matrix, to manual and automated approaches, edge‑case hunts, accessibility checks, security validation, and finally how autonomous, persona‑driven exploration surfaces bugs that scripted tests often miss.
---
Why Biometric Login Matters in React Native Apps
Security and User Expectations
Users now judge an app’s trustworthiness by how quickly and safely they can gain access. A biometric prompt that fails to appear, shows an incorrect label, or falls back to a password without clear feedback erodes confidence and drives abandonment. From a security standpoint, the biometric flow is often the gatekeeper to token storage, session creation, or payment initiation. If the flow can be bypassed or tricked, an attacker may gain unauthorized access to those downstream services. React Native adds a layer of complexity because the biometric decision is made in native code, yet the result is communicated back to JavaScript through a promise or callback. Any mismatch in handling—such as treating a null response as success—creates a vulnerability window that only appears under specific device states or OS versions.
Common Production Failures
In the wild, biometric login breaks in ways that unit tests rarely catch. A frequent issue is the authentication dialog being dismissed by the system because the app moved to the background while the prompt was visible, leaving the JavaScript side waiting indefinitely for a resolution. Another common failure occurs when a device policy (e.g., an MDM profile) disables biometrics after the app has already cached a biometric‑enabled flag, causing subsequent logins to skip the prompt and fall back to a hardcoded development token. Fingerprint sensors can also return a BIOMETRIC_ERROR_LOCKOUT after too many failed attempts, a state that many React Native wrappers do not surface, leading to silent login failures. Finally, accessibility services such as TalkBack or VoiceOver can interfere with the timing of the native dialog, causing double‑tap gestures to be interpreted as cancel actions. These scenarios only manifest when the app runs on real hardware with varied configurations, underscoring the need for a test matrix that spans happy paths, error paths, and edge conditions.
---
Test Matrix for Biometric Login
| Test Category | Description | Expected Result | Automation Feasibility | Notes |
|---|---|---|---|---|
| Happy Path – Fingerprint | Valid fingerprint presented, biometric module returns success | Login proceeds, token stored, UI navigates to home | High (mock native module) | Verify correct handling of promise resolution |
| Happy Path – Face ID | Valid face scan, module returns success | Same as fingerprint | High | Ensure platform branch works |
| Error – User Cancel | User taps cancel button on biometric prompt | App shows fallback login screen (PIN/password) | Medium (needs UI interaction) | Check that cancel error is distinguished from failure |
| Error – Biometric Not Enrolled | No fingerprint/face registered on device | Prompt to enroll biometrics or show fallback | Medium | Test on a clean device or after resetting biometrics |
| Error – Lockout After Too Many Attempts | Five consecutive failed attempts trigger lockout | App receives lockout error, shows fallback, does not retry biometric immediately | Low (requires real sensor) | Verify back‑off timing and UI messaging |
| Error – Sensor Unavailable | Sensor disabled via device policy or hardware fault | Prompt not shown, fallback offered immediately | Low (needs MDM or simulated fault) | Confirm that app does not crash |
| Edge – App Backgrounded During Prompt | User switches to another app while biometric dialog visible | Prompt dismissed, app receives cancellation/error, shows fallback | Medium (needs lifecycle handling) | Test with adb shell am pause or Xcode simulator background |
| Edge – Permission Revoked at Runtime | User goes to Settings and revokes biometric permission while app is in foreground | Next biometric attempt fails with permission error, fallback shown | Low (requires runtime permission toggle) | Observe that app does not retain stale success state |
| Accessibility – TalkBack Enabled | TalkBack active, user navigates to biometric button and double‑taps | Prompt announced correctly, login proceeds as usual | Medium | Verify that announcements are descriptive and not duplicated |
| Accessibility – Color Contrast | Biometric button meets WCAG AA contrast ratio | Visual inspection passes | Low (manual) | Important for low‑vision users |
| Security – No Biometric Data Leakage | Attempt to extract biometric template from app logs or storage | No biometric data present; only opaque result token | Low (requires forensic tools) | Confirm that native module never returns raw data |
| Security – Replay Resistance | Capture successful biometric response and replay it later | Replay attempt rejected, fallback triggered | Low (needs advanced tooling) | Ensure server‑side nonce or challenge is used |
| Fallback – PIN/Password Works | Biometric fails or is unavailable, user enters correct PIN/password | Login succeeds, token issued | High | Validate that fallback path shares same post‑login flow |
| Fallback – Invalid PIN/Password | Wrong credentials entered | App shows error, does not proceed | High | Ensure lockout timers apply if applicable |
*Automation feasibility* reflects how readily the scenario can be exercised in a CI pipeline using mocks, emulators, or device farms. Low feasibility items typically require real hardware or special state setup, making them candidates for periodic manual runs or persona‑driven exploration.
---
Manual Testing Approach Step‑by‑Step
Setting Up Test Devices (iOS/Android)
- Provision a clean device or emulator – For iOS, use a device with Touch ID/Face ID enabled and no existing biometric enrollment for the test account. For Android, ensure the device runs API 28+ with a fingerprint sensor or use the emulator’s fingerprint simulation (
adb -e emu finger touch). - Install the debug build – Use
npx react-native run-ios --deviceornpx react-native run-androidto load the latest code. - Enable developer logging – Add
console.logstatements around the biometric call or use Flipper to capture native bridge traffic.
Enrolling Biometrics
- iOS: Open Settings → Face ID & Passcode (or Touch ID & Passcode) → Set Up Face ID/Touch ID. Follow the prompts until enrollment completes.
- Android: Settings → Security → Fingerprint → Add fingerprint.
- Note: For lockout testing, intentionally fail enrollment five times using a wrong finger or face.
Executing Test Cases
Follow the matrix above, marking each step with a pass/fail. For UI‑driven steps (cancel, fallback), use the device’s physical buttons or on‑screen prompts. Keep a notebook of observations:
- Timing – Measure how long the biometric prompt stays visible before auto‑cancel (typically 30 s).
- Error propagation – Confirm that the error object returned to JavaScript matches the expected native error codes (
LAError.userCancel,LAError.biometryLockout, etc.). - State reset – After a failed biometric attempt, verify that the app does not retain a “biometric succeeded” flag in AsyncStorage or Redux.
Observing Logs and Crash Reports
- iOS: Use Console.app or
xcrun simctl spawn booted log show --predicate 'process == "YourApp"' --info. - Android: Run
adb logcat | grep -i biometricor use Android Studio’s Logcat. - Look for exceptions thrown from the native module (e.g.,
NSExceptionorRuntimeException) and ensure they are caught and translated into a user‑friendly message.
---
Automated Testing with React Native Specific Tools
Unit Tests with Jest and React Native Testing Library
Unit tests validate the JavaScript layer that prepares data for the biometric call and interprets the result. Mock the native module so you can control its resolution.
// __mocks__/react-native-touch-id.js
export default {
authenticate: jest.fn(),
isSupported: jest.fn(),
};
import TouchID from 'react-native-touch-id';
import { loginWithBiometrics } from '../src/auth';
describe('loginWithBiometrics', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('successful fingerprint leads to navigation', async () => {
TouchID.authenticate.mockResolvedValueOnce({ success: true });
const navigation = jest.fn();
await loginWithBiometrics(navigation);
expect(TouchID.authenticate).toHaveBeenCalled();
expect(navigation).toHaveBeenCalledWith('Home');
});
test('user cancel triggers fallback', async () => {
TouchID.authenticate.mockRejectedValueOnce({
code: LAError.userCancel,
});
const fallback = jest.fn();
await loginWithBiometrics(null, fallback);
expect(fallback).toHaveBeenCalled();
});
});
Run with npm test. This guarantees that promise handling, error branching, and state updates stay correct after refactors.
End‑to‑End with Detox
Detox drives the actual native UI, allowing you to test the biometric prompt interaction on simulators or real devices. Because the prompt is a system dialog, Detox cannot interact with it directly, but you can simulate the native response using the detox device APIs.
// e2e/biometric.login.spec.js
describe('Biometric login flow', () => {
beforeEach(async () => {
await device.launchApp({ newInstance: true });
});
it('should succeed with mocked fingerprint', async () => {
// Simulate successful fingerprint on Android emulator
await device.sendToApp({
type: 'fingerprint',
fingerPrintId: 1,
success: true
});
await expect(element(by.id('homeScreen'))).toBeVisible();
});
it('should show fallback after cancel', async () => {
await device.sendToApp({
type: 'fingerprint',
fingerPrintId: 1,
success: false,
cancel: true
});
await expect(element(by.id('loginWithPin'))).toBeVisible();
});
});
For iOS, use device.sendToApp({ type: 'touchId', match: true }) or match: false to emulate success/failure.
Mocking expo-local-authentication
If your project uses Expo, mock the module in Jest:
// __mocks__/expo-local-authentication.js
export default {
authenticate: jest.fn(),
hasHardwareAsync: jest.fn().mockResolvedValue(true),
isEnrolledAsync: jest.fn().mockResolvedValue(true),
};
Then write tests similar to the TouchID example.
CI Integration
Add a step in your CI (GitHub Actions, Bitrise, etc.) that runs the Detox suite on a device farm (Firebase Test Lab, AWS Device Farm). For the low‑feasibility edge cases (lockout, sensor disabled), schedule a weekly manual run or leverage a tool that can inject device policy changes via MDM APIs.
---
Edge Cases and Production‑Only Bugs
Biometric Lockout After Too Many Attempts
When the native sensor reaches its lockout threshold, the biometric module returns a specific error (BIOMETRIC_ERROR_LOCKOUT on Android, LAError.biometryLockout on iOS). Many React Native wrappers treat any non‑success as a generic failure, causing the app to immediately retry the biometric prompt, which frustrates users and may trigger a permanent lockout.
Test:
- Enroll a fingerprint.
- Fail authentication five times using an unregistered finger.
- On the sixth attempt, assert that the error object contains a lockout code and that the UI shows a message like “Try again in 30 seconds” or falls back to PIN.
Device Policy Changes (e.g., Admin Disabling Biometrics)
Enterprise MDM solutions can remotely disable biometrics. If your app caches a biometricsEnabled flag at startup, it will continue to attempt biometric auth after the policy change, leading to confusion.
Test:
- Use an MDM test profile (or Android’s
adb shell settings put secure lock_biometric_weak_enabled 0) to toggle biometrics off while the app is running. - Trigger a login and verify that the app immediately shows the fallback without displaying a biometric prompt.
Authentication Dialog Dismissal Timing
If the app moves to the background while the system biometric dialog is visible, iOS dismisses the prompt and returns LAError.userCancel. Android behaves similarly but may also return BIOMETRIC_ERROR_CANCEL. If your JavaScript does not distinguish cancel from failure, you might incorrectly treat it as a credential error and lock the user out.
Test:
- Start biometric login, press the home button (or switch to another app) after 2 seconds.
- Observe the error code and ensure the fallback path is triggered, not a permanent error state.
Background/Foreground Transition
Some apps perform token refresh in the background. If a biometric prompt is still pending when the app returns to the foreground, the native module may deliver a stale result, causing a race condition.
Test:
- Trigger biometric login, then quickly lock the device and unlock it after the prompt times out.
- Verify that the app does not incorrectly accept an outdated success response.
Permission Revocation at Runtime
Both platforms allow users to revoke biometric permission while an app is in the foreground. If the app does not re‑check permission before invoking the auth method, it may crash with a security exception.
Test:
- While at the biometric login screen, go to Settings → Privacy → [App] → Toggle off “Use Face ID/Touch ID” (iOS) or “Fingerprint” (Android).
- Tap the biometric button and confirm that the app handles the denial gracefully (shows fallback, does not crash).
Accessibility Overlays (TalkBack, VoiceOver)
Screen readers can inject additional touch events or change the timing of gestures. A double‑tap that should confirm biometric auth might be interpreted as a cancel if the accessibility service intercepts the event.
Test:
- Enable TalkBack (Android) or VoiceOver (iOS).
- Navigate to the biometric button and perform the activation gesture.
- Confirm that the prompt appears and that login proceeds as expected.
Biometric Sensor Failure (Dirty Finger, Poor Lighting)
Real‑world sensors can return transient errors like BIOMETRIC_ERROR_SENSOR_NOT_DETECTED or LAError.biometryNotAvailable. If your app treats these as permanent failures, users may be forced into fallback unnecessarily.
Test:
- Obstruct the fingerprint sensor with a cloth or cover the Face ID camera.
- Attempt login and verify that the app shows a retry option or a clear message (“Sensor not ready, try again”).
---
Accessibility and WCAG Considerations
Labeling Biometric Prompt
The native biometric dialog already includes a system‑provided reason string (passed via localAuthenticate or BiometricPrompt.Builder.setDescription). Ensure that this string is concise, localized, and does not rely on color alone to convey meaning.
Check:
- Verify that the reason string is read aloud by TalkBack/VoiceOver.
- Ensure that the contrast of any custom UI surrounding the biometric button meets WCAG AA (minimum 4.5:1 for normal text).
Alternative Login Paths
WCAG 2.1 Guideline 1.3.1 requires that information and relationships conveyed through presentation can be determined programmatically. If your app only offers biometric login, users who cannot use biometrics are blocked. Provide a visible, labeled alternative (PIN, password, or OTP) that is reachable via the same navigation hierarchy.
Test:
- Turn off biometrics on the device.
- Confirm that the fallback option is announced and operable without requiring gestures that depend on biometric hardware.
Screen Reader Announcements
When the biometric prompt appears, the screen reader should announce something like “Place your finger on the sensor to sign in” or “Look at the phone to unlock”. If your app overrides the default prompt with a custom modal, you must manually set accessibility labels.
Test:
- With TalkBack enabled, trigger biometric login.
- Listen for the announcement; it should be descriptive and not redundant.
---
Security and Privacy Testing
Ensuring No Biometric Data Leakage
Biometric templates never leave the Secure Enclave (iOS) or Trusted Execution Environment (Android). However, developers sometimes mistakenly log the result object or store it in AsyncStorage.
Test:
- Run the app with
adb logcator Console.app and filter for keywords like “fingerprint”, “face”, “touchID”. - Confirm that no raw biometric data appears in logs, network requests, or local storage.
Testing Fallback to PIN/Password
A secure biometric flow must gracefully degrade to a knowledge‑based factor when biometrics are unavailable or rejected. Verify that the fallback uses the same session‑management logic (e.g., issues a JWT after validation) and that the token is stored with the same security level (Keychain/Keystore).
Test:
- Disable biometrics via settings.
- Enter correct PIN/password and inspect network traffic to ensure the authentication endpoint receives the same payload as after a successful biometric login.
- Confirm that the token is stored in the secure storage layer appropriate to the platform (iOS Keychain, Android EncryptedSharedPreferences).
Secure Storage of Auth Tokens
After biometric validation, the app typically receives a session token from the backend. Ensure that this token is not written to plain‑text files or shared via console.log.
Test:
- Use a network interceptor (e.g., Flipper network plugin or
http-toolkit) to capture the token. - Then inspect the device’s file system (
run-ason Android, sandbox container on iOS) for any plain‑text copies of the token.
Resistance to Replay Attacks
If the backend only checks a static success flag from the biometric module, an attacker could replay a previously captured success response.
Test:
- Instrument the native module to output the raw result (for testing only) and capture it.
- Attempt to resend that captured payload to the login endpoint after a delay.
- Confirm that the server rejects the request (e.g., returns 401) due to missing nonce or timestamp.
---
Autonomous, Persona‑Driven Exploration with SUSATest
How SUSA Models Different User Personas
SUSATest’s autonomous agent loads the APK (or points at a web URL) and then drives the app using a set of behavior profiles. Each profile—curious, impatient, novice, adversarial, elderly, accessibility‑focused, power user—applies distinct timing, tap patterns, and error‑recovery tendencies. For biometric login, the agent will:
- Vary the time between launching the app and tapping the biometric button (from immediate to delayed after scrolling).
- Randomly cancel the prompt after different intervals to mimic an impatient user.
- Attempt to invoke biometric auth while TalkBack is active to emulate an accessibility user.
- Simulate low‑sensor conditions by covering the fingerprint area in the virtual environment (if supported).
What It Discovers That Scripts Miss
Scripted tests follow a predetermined sequence and often assume ideal conditions (device unlocked, sensor clean, no background interruptions). SUSATest’s exploratory nature surfaces issues such as:
- A race condition where tapping the biometric button twice in quick succession causes the native module to receive two concurrent auth requests, leading to a cryptic error that only appears under rapid‑tap personas.
- An edge case where the app’s state management incorrectly retains a “biometricSuccess” flag after a lockout, causing the next launch to skip the prompt entirely for a power‑user who repeatedly locks/unlocks the device.
- A scenario where an elderly persona’s slower tap triggers the system’s auto‑cancel timer, resulting in a perceived failure that novices never encounter because they tap faster.
Example Session Log
Below is a condensed excerpt from a SUSATest run on a sample React Native app that uses react-native-touch-id. The log shows the agent’s actions, the observed native responses, and the resulting verdict.
[Persona: Curious] 08:12:03 - Launched app, navigated to LoginScreen
[Persona: Curious] 08:12:05 - Tapped BiometricButton
[Native] BiometricPrompt shown (reason: "Sign in to ExampleApp")
[Persona: Curious] 08:12:07 - Waited 2s, then swiped up to home (backgrounded)
[Native] Prompt dismissed, returned LAError.userCancel
[Persona: Curious] 08:12:09 - App displayed fallback PIN screen (PASS)
[Persona: Impatient] 08:13:41 - Tapped BiometricButton, waited 0.3s, tapped again
[Native] Second call received while first pending → returned LAError.systemCancel
[Persona: Impatient] 08:13:43 - App showed generic error dialog, no fallback (FAIL)
[Persona: Elderly] 08:15:20 - Tapped BiometricButton, waited 5s (slow)
[Native] Prompt auto‑canceled after 30s? Actually returned LAError.userCancel after 5s due to system timeout? (Check)
[Persona: Elderly] 08:15:26 - App displayed fallback after timeout (PASS)
[Persona: Accessibility] 08:16:10 - TalkBack enabled, double‑tapped BiometricButton
[Native] Prompt announced, user placed finger, succeeded
[Persona: Accessibility] 08:16:13 - Login succeeded, home screen reached (PASS)
The log reveals that the Impatient persona uncovered a bug where rapid double‑taps caused a system‑cancel error that the app did not handle, leading to a missing fallback. This defect would likely stay hidden in a scripted test that always waits a fixed interval before proceeding.
---
Checklist for Biometric Login Testing
| ✅ Item | Description | How to Verify |
|---|---|---|
| Biometric prompt shows correct reason string | Reason is localized and readable | Listen with TalkBack/VoiceOver, inspect UI |
| Success path leads to intended screen | After valid biometric, app navigates to home or protected area | Observe navigation, check token storage |
| Cancel triggers fallback login | Tapping cancel shows PIN/password option | Tap cancel, verify fallback UI |
| Biometric not enrolled shows enrollment prompt or fallback | No biometric saved → system asks to enroll or offers alternative | Clear enrollment, attempt login |
| Lockout after too many failures shows appropriate message | After N failed attempts, app does not retry biometric immediately | Fail N+1 times, check UI and timing |
| Sensor unavailable (policy/hardware) skips prompt | When biometrics disabled by MDM or sensor fault, fallback appears immediately | Disable biometrics via settings, test login |
| App backgrounds during prompt handled gracefully | Switching away while prompt visible results in cancel/error and fallback | Press home button during prompt, observe outcome |
| Permission revoked at runtime leads to fallback | User turns off biometric permission while app in foreground | Revoke permission, tap biometric button |
| Accessibility services do not break flow | TalkBack/VoiceOver active, biometric login works | Enable service, complete login |
| No biometric data leaked in logs/storage | No raw fingerprint/face data appears in logs, AsyncStorage, or shared prefs | Search logs and storage for biometric keywords |
| Fallback to PIN/password uses same auth backend | Token issued after fallback matches token after biometric success | Compare network payloads, verify endpoint |
| Rate limiting / backoff applied after repeated failures | After many failed PIN attempts, app enforces delay or lockout | Attempt wrong PIN repeatedly, measure delay |
| Replay attack resisted | Replaying a captured biometric success is rejected by backend | Capture success, resend after delay, check server response |
| Secure storage of post‑login token | Token stored in Keychain/Keystore, not plain‑text files | Inspect file system for token copies |
| Manual exploratory runs catch persona‑specific bugs | Running SUSATest or similar yields new failures not in script suite | Execute autonomous agent, review new FAIL entries |
---
Closing Takeaways
Biometric login is a high‑impact feature that blends native security with JavaScript‑driven UI. In React Native, the surface area for failure is wide: the bridge, the native module, platform‑specific error codes, lifecycle events, accessibility overlays, and enterprise policies all influence whether a user can log in securely and quickly.
A thorough strategy begins with a clear test matrix that separates happy paths from error conditions and edge cases, then validates each layer—unit tests for decision logic, Detox‑driven E2E for UI interactions, and manual checks for states that are hard to emulate (lockouts, policy changes, sensor obstructions). Accessibility and security tests are not optional; they protect users with diverse needs and ensure that biometric data never leaves the secure enclave.
Finally, autonomous, persona‑driven exploration complements scripted testing by exercising the app in ways real users do: varying timing, mixing accessibility gestures, simulating sensor noise, and probing for race conditions that only appear under unusual but plausible usage patterns. Tools like SUSATest surface these hidden defects early, reducing the chance that a biometric‑related bug reaches production and erodes trust.
By combining the matrix, layered automation, manual diligence, and exploratory agents, you can ship a biometric login experience that feels seamless, works for every user, and resists both accidental failure and malicious abuse.
Test Your App Autonomously
Upload your APK or URL. SUSA explores like 10 real users — finds bugs, accessibility violations, and security issues. No scripts.
Try SUSA Free