---
title: Biometrics authentication for your mobile app
teaser: 'Practical approaches to add biometrics authentication to a mobile app.

  '
tags: react native,mobile,ios,android,authentication,security,development
author: Rakesh Arunachalam
published_on: 2026-05-25
---

Biometrics (Face ID, Fingerprint and more) authentication have quickly become
something users expect from a modern mobile app. These are fast, familiar, and
removes friction that comes with typing passwords on the phone. With React
Native, biometrics authentication can be an easy feature or a tricky one
depending on the options you take. There are multiple ways to build it, and they
do not all offer the same level of security.

In this blog post, I'm going to share three approaches that we evaluated for a
recent project. All three approaches can enable a smooth authentication
experience, but they come with different tradeoffs in terms of implementation
complexity, user experience, and security implications. If you are building this
feature for a banking/financial services app, a healthcare app, or a product
handling sensitive user data, these tradeoffs matter a lot. In this post, for
each approach of biometrics authentication, we'll explore how they work, where
they fall short, and when each makes sense.

## Simple Prompt (Approach 1)

The simplest way to add biometrics to your app is to use it as a UI level gate:

- Prompt the user to authenticate with their biometrics (face or fingerprint).
- On success, read the auth token from secure storage

This is what
[simplePrompt](https://github.com/sbaiahmed1/react-native-biometrics#simpleprompt)
in `react-native-biometrics` does. It triggers the native biometric dialog and
returns a `boolean` indicating **success** or **failure**. The critical thing to
understand about this approach is that the biometric check and the token
retrieval are two separate, unlinked operations. The app calls `simplePrompt`,
gets back `true`, then independently reads the token from storage. There is no
cryptographic connection between them and the biometric result is merely a
signal to our code, not a hardware enforced gate on the data itself.

```javascript
// Pseudocode showing how this works
const success = await biometrics.prompt('Confirm your identity')

if (success) {
  const token = await secureStorage.read('auth_token')
  // log user in
}
```

### Expo vs React Native CLI (Approach 1)

With [Expo](https://expo.dev/) managed workflow, you'd reach for
[expo-local-authentication](https://docs.expo.dev/versions/latest/sdk/local-authentication/)
rather than `react-native-biometrics`. The API is nearly identical, returns a
result object with a `success` boolean, and it works without ejecting or adding
any native configuration. To store and retrieve the auth token securely, pair it
with
[expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/),
which writes to the iOS Keychain and Android Keystore under the hood.

With [React Native
CLI](https://reactnative.dev/docs/getting-started-without-a-framework) (bare
workflow),
[react-native-biometrics](https://github.com/sbaiahmed1/react-native-biometrics)
is the natural choice for the biometric prompt. For token storage, the
equivalent is
[react-native-keychain](https://github.com/oblador/react-native-keychain), which
gives you direct access to the iOS Keychain and Android Keystore and supports
more granular security configuration than `expo-secure-store`.

### Pros (Approach 1)

- Easy to implement with minimal boilerplate and works out of the box on both
  platforms
- Supports Face ID, Touch ID, and fingerprint without extra configuration
- Works across iOS and Android with a consistent API
- No backend required
- Multi device support as each device independently manages its own stored token

### Cons (Approach 1)

- Biometric enforcement happens at the App level, not the OS or hardware level.
  The biometric check and the token read are two independent steps with no
  cryptographic coupling between them
- On a jailbroken or rooted device, tools like [Frida](https://frida.re/) can be
  used to hook into the app at runtime, force `simplePrompt` to return `true`,
  and retrieve the token without the user actually authenticating
- Does not meet
  [FIDO2](https://www.microsoft.com/en-us/security/business/security-101/what-is-fido2)
  or WebAuthn standards as there is no challenge-response, no server side
  verification, and no cryptographic proof that a real biometric event occurred
  on the device
- The stored token is not hardware locked. Code that bypasses the biometric gate
  can read it directly from storage
- Not appropriate for apps handling sensitive financial, healthcare, or
  regulated data

## Cryptographic Key Pair (Approach 2)

This approach upgrades biometrics from a UI gate to something the server can
independently verify. The
[`createKeys()`](https://github.com/sbaiahmed1/react-native-biometrics#-key-management)
method in `react-native-biometrics` generates an asymmetric key pair. The device
holds a private key locked behind biometrics and the server holds the matching
public key. Login is just the device signing a challenge and the server
verifying the signature.

What makes each piece do its job:

- **Private key** — Created inside the device chip at enrollment and never
  exported. It is only unlocked by a passing biometric scan.
- **Public key** — Returned to the app and registered on the server. It is used
  only to verify signatures.
- **Challenge** — A one-time UUID issued by the server at login. The server
  enforces an `expires_at` field so late or replayed submissions are rejected.
- **Signature** — The chip's signed challenge sent to the server. The server
  checks it against the stored public key and issues an access token.

The server never learned your biometric, your private key, or even your
password. It just confirmed the right chip signed the right challenge at the
right time.

### Enrollment

The device generates a key pair inside the hardware chip. The private key never
leaves. The public key gets sent to the server.

*Private key example:* Stays on device, locked by biometrics, never transmitted.

```text
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA2a2rwplBQLF29amygykEMmYz0+Ik9e8bKGFCNSGBaGMq
mMV5FBtLGMGMmFnMjQKFMkBHiNIEuAsYhVMqAC+XAAA...
-----END RSA PRIVATE KEY-----
```

*Public key example:* Sent to server at enrollment, stored against the user's
account.

```text
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2a2rwplBQLzamygyk
EMmYz0+Ik9e8bKGFCNSGBaGMqmMV5FBtLGJmFnMjQKF...
-----END PUBLIC KEY-----
```

### Authentication

**Step 1:** User on the device says: "I want to log in."

**Step 2:** Server generates a fresh challenge and sends it back:

```json
{
  "challenge": "a3f8c2d1-9b4e-4f7a-8c3d-2e1f0a9b8c7d",
  "expires_at": "2026-05-01T10:00:30Z"
}
```

Just a random UUID. Expires in 30 seconds, useless after that.

**Step 3:** Device prompts Face ID / fingerprint.

Biometric passes → chip unlocks private key → signs the challenge:

```json
{
  "challenge": "a3f8c2d1-9b4e-4f7a-8c3d-2e1f0a9b8c7d",
  "signature": "XrAGWHSPpnmqzQxL3kVt9Yw2NcBdEfUoRiMaTlKsPqJhGvCxZnWbOuIyDe..."
}
```

This is sent over the network. No password or token or private key. Just the
challenge and its signature.

**Step 4:** Server verifies:

```text
verify(
  publicKey  = <the one stored at enrollment>,
  challenge  = "a3f8c2d1-9b4e-4f7a-8c3d-2e1f0a9b8c7d",
  signature  = "XrAGWHSP..."
) → ✅ true
```

Server responds:

```json
{
  "access_token": "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyJ9...",
  "expires_in": 720 // 12 hours
}
```

### Expo vs React Native CLI (Approach 2)

`expo-local-authentication` implements the simple prompt pattern from Approach 1
and exposes no key management APIs. There is no `createKeys()` or
`createSignature()` equivalent in the Expo SDK, so you cannot implement the
cryptographic flow with the standard managed workflow or Expo Go.

With React Native CLI, `react-native-biometrics` links natively on install and
gives you direct access to `createKeys()`, `createSignature()`, and
`deleteKeys()`. Your server needs three endpoints: one to store the public key
at enrollment, one to issue a challenge, and one to verify the signature and
return an access token.

### Pros (Approach 2)

- The biometric check and authentication are cryptographically coupled: a valid
  signature can only be produced if the biometric passes, enforced by the
  hardware chip, not App code.
- Protects against replay attacks: every challenge is a one-time UUID with a
  short expiry window.
- Closest to
  [FIDO2](https://www.microsoft.com/en-us/security/business/security-101/what-is-fido2)
  of the three approaches with hardware backed keys, biometric gated signing,
  and server-side challenge-response compared to the other approaches.
- Resistant to runtime hooking tools like [Frida](https://frida.re/) as any
  spoofed signature here fails server-side verification because producing a
  valid signature requires the private key, which never leaves the hardware
  chip.

### Cons (Approach 2)

- More complex to implement than Approach 1 as it requires backend work for key
  enrollment, challenge-response, and server-side verification.
- Multi device support is slightly complex as it requires the server to manage,
  store, and revoke a separate public key for every enrolled device per user.
- `expo-local-authentication` does not support this flow; Expo users need a bare
  React Native workflow, which adds build infrastructure overhead compared to
  the other two approaches.

## Keychain (Approach 3)

This approach sits between the previous two. Like Approach 1, the auth token
lives on the device. Like Approach 2, the biometric check is enforced by the OS
at the hardware level, not by the React Native JS code.

When the token is written to the [iOS
Keychain](https://developer.apple.com/documentation/security/keychain_services)
or [Android
Keystore](https://developer.android.com/privacy-and-security/keystore) with a
biometric access control flag, the OS will refuse to decrypt and return the
value unless a successful biometric scan happens. The biometric prompt and the
token decryption become a single OS-level operation. The App does not call a
separate prompt API and then read the value, it just reads the value, and the OS
shows the biometric prompt as part of completing that read.

```javascript
// Pseudocode showing how this works
const token = await secureStorage.read('auth_token', {
  requireAuthentication: true,
})
// log user in
```

In practice, most apps store only a refresh token behind this gate and keep the
access token in memory. The refresh token is long lived but never leaves the
device, while the access token is short lived (~12 hours), lives in memory only,
and is obtained by exchanging the refresh token at the start of each session. If
an access token leaks, it expires quickly on its own. The refresh token, the
more powerful credential, is hardware protected and never transmitted after the
initial login.

### Expo vs React Native CLI (Approach 3)

With Expo,
[expo-secure-store](https://docs.expo.dev/versions/latest/sdk/securestore/)
implements this pattern via `requireAuthentication: true`. The biometric prompt
fires automatically on read, and `SecureStore.canUseBiometricAuthentication()`
lets you check up front whether the device has biometrics enrolled before opting
the user into biometric sign in. No native modules to install, and it works
inside the managed workflow.

With React Native CLI,
[react-native-keychain](https://github.com/oblador/react-native-keychain) gives
the same OS enforced gate but with more granular controls. You pass
`accessControl: ACCESS_CONTROL.BIOMETRY_CURRENT_SET` and `securityLevel:
SECURITY_LEVEL.SECURE_HARDWARE` to `setGenericPassword`, which guarantees the
key is stored securely.

### Pros (Approach 3)

- Biometric enforcement happens at the OS/hardware level, not the App level. A
  patched App using tools like Frida that bypasses the biometric call still
  cannot read the token because the OS will not release the encryption key.
- No backend work required, unlike Approach 2. Only the token storage layer
  changes, the rest of the auth stack stays the same.
- Works inside the Expo managed workflow via `expo-secure-store`, so we don't
  need a Bare workflow for this to be implemented.
- Multi device support is easy. Each device stores its own copy of the token
  encrypted securely.
- `BIOMETRY_CURRENT_SET` invalidates the stored token automatically when a new
  biometric is enrolled, which is a sensible default for apps handling sensitive
  data.

### Cons (Approach 3)

- The token still lives on the device. If the access token is compromised
  through a network breach or server side leak, biometrics on the device cannot
  help. This can be mitigated by using a refresh token.
- Token expiry and rotation has to be handled in the App, typically by pairing
  this with a refresh token pattern explained above.
- There is no server side proof that biometrics were used. The server sees a
  normal token request and trusts the client. Approach 2 is the only one of the
  three that gives the server, cryptographic evidence of a real biometric event.
- Does not meet
  [FIDO2](https://www.microsoft.com/en-us/security/business/security-101/what-is-fido2)
  or WebAuthn standards as there is no challenge-response and no asymmetric key
  pair, just hardware backed symmetric storage.

## Conclusion

| | Approach 1 | Approach 2 | Approach 3 |
| --- | --- | --- | --- |
| Biometric enforcement | App (JS) | Hardware chip | OS / hardware |
| Frida / patched app bypass | Vulnerable | Resistant | Resistant |
| Backend required | No | Yes | No |
| Server-side biometric proof | No | Yes | No |
| Replay attack protection | No | Yes | No |
| Multi-device support | Easy | Complex | Easy |
| Expo managed workflow | Yes | No | Yes |
| FIDO2 / WebAuthn alignment | No | Closest | No |

If your app is relatively simple (a productivity tool, social app or utility),
and biometrics is primarily a convenience feature for a fast login, Approach 1
is a reasonable choice. The implementation is straightforward and the tradeoffs
are acceptable when there is no sensitive financial or personal data at stake.

If you are building a banking app, a financial services product, a healthcare
app, or anything that handles regulated personal data, the bar is higher.
Approach 1 is not appropriate here. Between Approaches 2 and 3, the deciding
factor is usually team capability and backend access. Approach 3 requires no
backend changes, making it the practical default for most teams. Approach 2 is
the strongest option but requires a backend. If your team has the backend access
and is comfortable with the added complexity, Approach 2 is the right call for
maximum security for the mobile app.

In practice, most apps in sensitive industries start with Approach 3 and move
toward Approach 2 if compliance requirements or a security audit demands server
side verification.
