Biometric Authentication in Mobile Banking
bankingAugust 3, 2026

Biometric Authentication in Mobile Banking

A React Native Implementation Guide

Face ID and fingerprint prompts feel simple from the user's side: touch a sensor, look at a camera, you're in. Underneath, a banking app implementing biometric authentication has to get several things right simultaneously — hardware-backed key storage, a fallback path that satisfies PSD2 Strong Customer Authentication (SCA), and a UX flow that doesn't quietly downgrade security the moment biometrics fail. This guide walks through the implementation from a React Native frontend developer's perspective, including the parts that are easy to get wrong. 


Why "Just Add Face ID" Isn't the Spec 

A naive implementation treats biometric authentication as a local gate: prompt the user, get a boolean back, unlock the app if true. This pattern is common in consumer apps and is a liability in banking. The boolean from LocalAuthentication.authenticateAsync() or react-native-biometrics tells you the device's biometric sensor matched a locally enrolled face or fingerprint. It says nothing to your backend. If the only thing gating access to a funds transfer is a client-side boolean, an attacker who compromises the app's local storage or intercepts the unlock event has everything they need. 


The correct architecture treats the biometric prompt as a local authorization gate to unlock a cryptographic key, not as the authentication event itself. The authentication event is the signature that key produces, verified server-side. 


Secure Enclave and Keystore: The Actual Trust Anchor 

On iOS, private keys can be generated inside the Secure Enclave, a hardware-isolated coprocessor that never exposes the raw key material to the OS, the app, or Apple itself. On Android, the equivalent is the Android Keystore backed by a Trusted Execution Environment (TEE) or StrongBox on supported devices. 

In React Native, you won't call these APIs directly — you go through a native module. react-native-keychain and react-native-biometrics both wrap the underlying platform primitives, but their guarantees differ meaningfully: 

  • react-native-biometrics generates an asymmetric keypair (RSA or ECDSA) inside the Secure Enclave/Keystore, and exposes a createSignature() method that requires biometric confirmation before it will sign a payload. This is the pattern you want for authentication — the private key never leaves hardware, and every signature is proof that a live biometric check occurred at that moment. 


  • react-native-keychain is primarily a secure storage wrapper (Keychain on iOS, EncryptedSharedPreferences/Keystore on Android) for tokens or credentials, gated by biometric or device passcode. It's the right tool for storing a refresh token, not for producing a signed authentication assertion. 


For SCA-grade authentication, you want the signature-based approach: 

 1 import ReactNativeBiometrics from 'react-native-biometrics'; 
 2  
 3 const rnBiometrics = new ReactNativeBiometrics({ 
 4   allowDeviceCredentials: true, // enables PIN/passcode fallback at the OS level 
 5 }); 
 6  
 7 async function enrollBiometricKey(userId) { 
 8   const { available, biometryType } = await rnBiometrics.isSensorAvailable(); 
 9   if (!available) { 
10     throw new Error('BIOMETRICS_UNAVAILABLE'); 
11   } 
12  
13   const { publicKey } = await rnBiometrics.createKeys(); 
14  
15   // Register the public key against the user's session server-side. 
16   // The private key never leaves the Secure Enclave/Keystore. 
17   await api.post('/auth/biometric/enroll', { userId, publicKey, biometryType }); 
18 } 

At authentication time, the app requests a signature over a server-issued challenge, not over a static payload: 

 1 async function authenticateWithBiometrics(userId) { 
 2   const { challenge } = await api.post('/auth/biometric/challenge', { userId }); 
 3  
 4   const { success, signature } = await rnBiometrics.createSignature({ 
 5     promptMessage: 'Confirm your identity', 
 6     payload: challenge, 
 7   }); 
 8  
 9   if (!success) { 
10     throw new Error('BIOMETRIC_AUTH_CANCELLED'); 
11   } 
12  
13   return api.post('/auth/biometric/verify', { userId, challenge, signature }); 
14 } 

The server verifies the signature against the enrolled public key. Factors like "possession" and "inherence" are at this point "checked" under SCA, the private key is bound to hardware the user possesses, and the signature could only be produced after a biometric check. 


Mapping to PSD2 SCA Factor Requirements 


PSD2 SCA requires authentication to combine at least two independent elements from: knowledge (something the user knows), possession (something the user has), and inherence (something the user is). A well-implemented biometric flow in a banking app typically maps as follows: 


Possession

The device itself, represented by the hardware-bound private key that cannot be exported or cloned. 


Inherence

The biometric check gating access to that key. 


Knowledge

Usually established at initial login (PIN, password) and persisted via a device-bound session, not re-collected on every biometric unlock — this is what makes the biometric flow feel fast without breaking SCA. 


This is also where dynamic linking matters for payment authorization specifically: for transaction-triggering actions (not just app unlock), the signed challenge should include the transaction amount and payee, not a generic nonce, so the signature is cryptographically tied to that specific payment instruction. This is a common gap — teams implement biometric login correctly but reuse the same generic auth flow for transaction confirmation, which does not satisfy SCA's dynamic linking requirement for payments. 


 1 async function authorizePayment(paymentDetails) { 
 2   const { challenge } = await api.post('/payments/challenge', paymentDetails); 
 3   // challenge here is derived server-side from amount + payee + timestamp 
 4  
 5   const { success, signature } = await rnBiometrics.createSignature({ 
 6     promptMessage: `Confirm payment of ${paymentDetails.amount} to ${paymentDetails.payee}`, 
 7     payload: challenge, 
 8   }); 
 9  
10   if (!success) throw new Error('PAYMENT_AUTH_CANCELLED'); 
11  
12   return api.post('/payments/authorize', { ...paymentDetails, challenge, signature }); 
13 } 

 

Fallback Flows That Don't Undermine SCA 


Biometric sensors fail: dirty screens, wet fingers, enrollment changes, hardware faults. Your fallback path needs to preserve the same factor combination, not quietly drop to a single factor. 


A few implementation rules worth enforcing: 

  • Don't fall back to "remember this device" alone. A device-remembered flag is possession only. If biometrics fail, fall back to PIN/passcode entry (knowledge) plus the existing device-bound key (possession) — not to skipping authentication. 
  • Detect biometric enrollment changes. Both platforms expose a way to detect if enrolled biometrics changed since key creation (biometryKeysExist checks, or comparing keychainAccessible state). If the enrolled face/fingerprint set changes, treat the existing key as invalidated and force re-enrollment through full-strength authentication — otherwise a new fingerprint added to the device could silently gain access to an existing session. 
  • Rate-limit and log fallback attempts server-side. The client should never be the sole enforcer of retry limits or lockout logic; treat every client-reported failure as informational only, and enforce the actual lockout policy against the account server-side. 
  • Exclude device credential fallback for high-risk actions where mandated. allowDeviceCredentials: true on react-native-biometrics will let the OS present a PIN/pattern fallback automatically. This is appropriate for app unlock but should typically be disabled and replaced with an explicit, separately-designed step-up flow for high-value transaction authorization, where you want to control exactly what "knowledge factor" evidence is being collected. 


What to Test Before You Ship 

Beyond standard unit and integration tests, a few scenarios are specific to this flow and easy to overlook in QA: 

Key invalidation after biometric enrollment changes on both iOS and Android 

App behavior when biometrics are available but the user has never enrolled any 

Behavior across app backgrounding mid-authentication (a common source of stuck states with native biometric modules) 

Server-side signature verification failure paths — confirm the app surfaces a generic error rather than one that reveals whether the failure was key-mismatch versus a network issue 


Biometric authentication done well in a banking context is not a UI feature — it's a cryptographic protocol with a friendly front end. Getting the enrollment, signature, and fallback logic right up front avoids the more expensive fix later: retrofitting SCA compliance into a flow that was designed around a boolean.