In a React Native brokerage app, why is checking authenticateAsync before reading a stored refresh token weaker than protecting the item with a biometric access-control flag?
answer
- a boolean in JS vs a key in the OS
- hooking can flip a JavaScript branch
- biometryCurrentSet on the Keychain item
- setUserAuthenticationRequired on the Keystore key
- new fingerprint invalidates the item
basics
~20 sauthenticateAsync only returns a success value to JavaScript; the token stays readable without it. A biometric access-control flag makes the Keychain or Keystore itself refuse to release or decrypt the token until the OS has verified the user.
solid answer
~40 sWith the prompt-then-read pattern, `authenticateAsync` is a **UI gate**: the token sits in secure storage readable by the app at any time, and the only thing stopping a read is an `if` in JavaScript that instrumentation on a rooted or jailbroken device can bypass. With an **access-control flag** the OS enforces the gate: on iOS the Keychain item is created with `biometryCurrentSet`, on Android the Keystore key with `setUserAuthenticationRequired(true)` and unlocked through the biometric prompt. In Expo that is `expo-secure-store`'s `requireAuthentication`. The cost is real: adding a fingerprint or changing Face ID invalidates the item, so reads return `null` and the user must sign in with a password again.
code
typescript · 25 linesimport * as LocalAuthentication from 'expo-local-authentication';
import * as SecureStore from 'expo-secure-store';
// UI gate only: the item itself is readable without the prompt.
export async function readAfterPrompt(): Promise<string | null> {
const result = await LocalAuthentication.authenticateAsync({ promptMessage: 'Unlock trading' });
if (!result.success) return null;
return SecureStore.getItemAsync('refreshToken');
}
// OS-enforced: the Keychain item / Keystore key requires biometric authentication.
export async function saveBoundToken(token: string): Promise<void> {
await SecureStore.setItemAsync('refreshToken.bio', token, {
requireAuthentication: true,
authenticationPrompt: 'Unlock trading',
});
}
export async function readBoundToken(): Promise<string | null> {
// null after a biometric change has invalidated the item: send the user to password sign-in.
return SecureStore.getItemAsync('refreshToken.bio', {
requireAuthentication: true,
authenticationPrompt: 'Unlock trading',
});
}go deeper
Remember that a biometric prompt returning success is not the same as the token being protected by biometrics.
Name the two patterns and which component enforces each: JavaScript for prompt-then-read, the Keychain or Keystore for an access-control flag.
Design for the consequences: invalidation on biometric change, cancelled reads, real-device testing, and a password path that re-stores a fresh token.
Decide per operation how much assurance is needed, balancing re-login friction after biometric changes against the value of an OS-enforced gate on money movement.
## Two ways to put biometrics in front of a token A brokerage app wants the refresh token used only after Face ID or a fingerprint. There are two common implementations, and interviewers ask about them because they look the same on screen and are very different underneath. 1. **Prompt, then read.** Store the token in secure storage normally. At unlock, call `LocalAuthentication.authenticateAsync()` from `expo-local-authentication`; if it resolves with `success: true`, read the token. 2. **Bind the item to biometrics.** Store the token with an **access-control flag**, so the operating system itself demands biometric authentication before it releases the item or lets its key decrypt. ## Why prompt-then-read is only a UI gate - The stored item has no biometric requirement; the app can read it at any moment, with or without the prompt. - The decision lives in JavaScript: a resolved object with a `success` field and an `if` statement. - On a rooted or jailbroken device, runtime instrumentation can patch that branch or fake the result, and the read then proceeds as usual. - On Android, `expo-local-authentication` shows the biometric prompt without attaching any cryptographic operation to it, so a successful scan unlocks nothing by itself. That is fine for a **UX lock** (hiding balances when the app returns to the foreground). It is weak as **protection of the credential**. ## What an access-control flag changes | Platform | Flag | Who enforces it | |---|---|---| | iOS | Keychain item created with a `SecAccessControl` using `biometryCurrentSet` | the Keychain releases the item only after the OS verifies a currently enrolled biometric | | Android | Keystore key generated with `setUserAuthenticationRequired(true)` | the key can decrypt only inside an authenticated biometric prompt that carries the cipher operation | In an Expo project `expo-secure-store` exposes both as its `requireAuthentication` option; `react-native-keychain` offers equivalent access-control settings. Either way, JavaScript never sees a success flag it could be tricked about: the read either returns the token after the OS check or fails. ## The behaviours you must design for - **Biometric changes invalidate the item.** `biometryCurrentSet` is tied to the enrolled set, and on Android the key is permanently invalidated. `expo-secure-store` then resolves reads with `null`. The app must fall back to a full sign-in with the password and store a fresh token. - **No passcode shortcut on iOS.** `biometryCurrentSet` accepts the current biometrics, not the device passcode, so a user without working Face ID needs your password path. - **Prompts on different operations.** In `expo-secure-store`, iOS asks for authentication when reading or updating an existing item but not when creating it; Android asks for every operation. - **A cancelled prompt fails the read.** Handle the rejection and keep the user on the lock screen. - **Do not use the synchronous getter.** `expo-secure-store`'s synchronous `getItem` blocks the JavaScript thread while the biometric prompt is up; use the async variant. - **Real devices only.** Simulators and emulators do not enforce the biometric check on retrieval. ## Testing the difference Because simulators do not enforce the check, verify on physical devices: 1. Store a bound token, kill the app, and confirm a read shows the system prompt. 2. Cancel the prompt and confirm the app stays on its lock screen instead of crashing. 3. Enroll an extra fingerprint or reset Face ID, then confirm the read returns `null` and the password path stores a fresh token. 4. Trigger lockout with repeated failed scans and confirm the password path still works. ## Choosing per operation - **Unlocking the portfolio view**: prompt-then-read, or even no biometric at all, may be enough; the refresh token is still in secure storage. - **Using the refresh token or confirming an order**: bind the token (or a separate step-up credential) with an access-control flag. - **Server side**: a device check is never the whole story; the server still validates and can revoke the refresh token. The short version for an interview: a prompt proves the user was present to your JavaScript; an access-control flag makes the operating system refuse to hand over the secret otherwise.
- A user adds a second fingerprint and the brokerage app suddenly asks for the password again. Bug or feature?Feature. A biometric-bound item is tied to the enrolled set, so a new fingerprint invalidates it, and `expo-secure-store` returns `null`. That stops someone who can add their own fingerprint to an unlocked phone from inheriting the token. The app should explain why and re-store a fresh token after the password sign-in.
- Is there still a place for `authenticateAsync` if the token is biometric-bound?Yes, as a UX lock that does not touch the credential, such as blurring balances when the app returns from the background, or when you need a presence check for something that has no stored secret behind it. It should not be the only thing standing between the app and the refresh token.
Prompt-then-read is a receptionist who checks your badge and then points at an unlocked cabinet; bribe the receptionist and the cabinet opens. An access-control flag puts the badge reader on the cabinet itself.
saying these in an interview costs you the question
- A successful authenticateAsync call decrypts the stored token.
- Checking result.success in JavaScript cannot be bypassed.
- A biometric-bound item keeps working after a new fingerprint is added.
- Biometric-protected storage can be verified in the iOS Simulator.
- On iOS a biometryCurrentSet item also opens with the device passcode.