





























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.
The simplest way to add biometrics to your app is to use it as a UI level gate:
This is what
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.
// 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
}
With Expo managed workflow, you’d reach for
expo-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,
which writes to the iOS Keychain and Android Keystore under the hood.
With React Native
CLI (bare
workflow),
react-native-biometrics
is the natural choice for the biometric prompt. For token storage, the
equivalent is
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.
simplePrompt to return true,
and retrieve the token without the user actually authenticatingThis approach upgrades biometrics from a UI gate to something the server can
independently verify. The
createKeys()
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:
expires_at field so late or replayed submissions are rejected.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.
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.
-----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.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2a2rwplBQLzamygyk
EMmYz0+Ik9e8bKGFCNSGBaGMqmMV5FBtLGJmFnMjQKF...
-----END PUBLIC KEY-----
Step 1: User on the device says: “I want to log in.”
Step 2: Server generates a fresh challenge and sends it back:
{
"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:
{
"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:
verify(
publicKey = <the one stored at enrollment>,
challenge = "a3f8c2d1-9b4e-4f7a-8c3d-2e1f0a9b8c7d",
signature = "XrAGWHSP..."
) → ✅ true
Server responds:
{
"access_token": "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyXzEyMyJ9...",
"expires_in": 720 // 12 hours
}
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.
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.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 or Android 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.
// 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.
With Expo,
expo-secure-store
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 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.
expo-secure-store, so we don’t
need a Bare workflow for this to be implemented.BIOMETRY_CURRENT_SET invalidates the stored token automatically when a new
biometric is enrolled, which is a sensible default for apps handling sensitive
data.| 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.
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。