Biometric Authentication in Mobile Banking
A React Native Implementation Guide
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.
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.
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:
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.
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:
The device itself, represented by the hardware-bound private key that cannot be exported or cloned.
The biometric check gating access to that key.
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:
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.