In a React Native app, what should logout wipe from the device, and why can an iOS reinstall still find the previous user's refresh token?
answer
- revoke first, then delete locally
- one delete per secure-store key
- Keychain can outlive the uninstall
- Keystore key dies with the app
- first-launch marker in wipeable storage
basics
~20 sLogout should revoke the refresh token server-side, then delete every secure-storage entry and clear in-memory and persisted user data. iOS Keychain items can survive an uninstall, so a first-launch check should wipe leftovers from a previous install.
solid answer
~40 sA complete logout does three things: ask the server to **revoke** the refresh token while the app still holds it, **delete** each credential key from secure storage (for example one `SecureStore.deleteItemAsync` per key, with the same `keychainService` used to write it), and clear in-memory auth state and any persisted user data. The reinstall trap is iOS-specific: **Keychain items can persist across uninstall** when the app is reinstalled with the same bundle ID, whereas Android's Keystore key is deleted with the app. So on first launch the app checks a marker kept in storage the uninstall does erase, such as AsyncStorage; if the marker is missing, it wipes the Keychain entries before anything reads them.
code
typescript · 20 linesimport { createAsyncStorage } from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';
const appStorage = createAsyncStorage('app');
const CREDENTIAL_KEYS = ['refreshToken', 'refreshToken.bio'] as const;
const INSTALL_MARKER = 'installMarker';
export async function wipeCredentials(): Promise<void> {
await Promise.all(CREDENTIAL_KEYS.map((key) => SecureStore.deleteItemAsync(key)));
}
// Run before anything reads a credential: a fresh install has no marker,
// but the iOS Keychain may still hold items from the previous install.
export async function wipeLeftoversAfterReinstall(): Promise<void> {
const marker = await appStorage.getItem(INSTALL_MARKER);
if (marker === null) {
await wipeCredentials();
await appStorage.setItem(INSTALL_MARKER, '1');
}
}go deeper
Recall that logout deletes the stored tokens, not just the screen, and that iOS Keychain items can outlive the app.
List the full logout sequence and explain the uninstall difference between the Keychain and a Keystore-backed store.
Show the first-launch wipe with a marker in wipeable storage, failure handling for deletes and revocation, and per-account key namespacing.
Discuss where device-side wiping stops being enough and server-side session management, short token lifetimes and remote sign-out take over.
## Logout is more than navigating to the sign-in screen In a brokerage app, "log out" must mean the device can no longer act as the user. Resetting the navigation stack while the refresh token still sits in secure storage is the classic defect: the next person to open the app, or anyone who extracts the storage, can keep minting access tokens. ## What a complete logout does 1. **Revoke on the server first.** While the app still holds the refresh token, call your backend's logout or revocation endpoint so the token is dead even if a copy survives somewhere. Do not block the local wipe on that call: if the network fails, still wipe, and let the server's expiry do the rest. 2. **Delete every credential key from secure storage.** `expo-secure-store` has no clear-all call, so keep a list of the keys you write and delete each one. Pass the same `keychainService` you used when writing, or the delete targets a different item. Deleting does not need a biometric prompt, even for an item written with `requireAuthentication`. 3. **Clear in-memory state.** The access token, the user object and any auth context value. 4. **Clear persisted user data.** Cached portfolio responses, a persisted store, downloaded statements: anything tied to the account. 5. **Reset biometric-unlock settings** that were per user, so the next account starts clean. Treat a failed delete as a real error: the promise rejects, and a logout that silently leaves the token behind is worse than one that tells the user something went wrong. ## Why the token can come back after reinstall on iOS The two platforms handle uninstall differently: | | iOS Keychain | Android Keystore-backed storage | |---|---|---| | On uninstall | items **may remain** in the Keychain | the Keystore key is **deleted** with the app | | On reinstall (same bundle ID / package) | the old item can be read again | old ciphertext, if restored, cannot be decrypted; `expo-secure-store` removes it and returns `null` | Expo's documentation states the iOS behaviour and adds that it is not guaranteed, so you should not rely on it either way. The practical consequence: a user who "logged out" by deleting the app, or a phone passed to someone else who reinstalls the app, may land straight in the old account. ## The first-launch wipe The fix uses a store that the uninstall *does* clear: - Keep a small **install marker** in AsyncStorage or a plain file in the app's sandbox; both are removed on uninstall on iOS. - At startup, before any code reads a credential, check the marker. - If it is missing, this is a fresh install: delete every Keychain entry the app may have written, then write the marker. - If it is present, proceed normally. The marker is not sensitive, so AsyncStorage is the right tool for it; the point is precisely that it does *not* survive uninstall. ## Related platform details - **Device migration.** A Keychain item with a `ThisDeviceOnly` accessibility class is not migrated to a new phone restored from a backup, which avoids a similar ghost login on a new device. - **Android backups.** `expo-secure-store`'s config plugin excludes its data from Android Auto Backup when the app has no custom backup rules, because restored ciphertext without its Keystore key is unreadable. - **Account switching.** If the app supports several accounts, namespace the keys per account and delete only the one being signed out, or wipe all on a full logout. ## Common mistakes - Setting the token to an empty string instead of deleting the item. - Deleting with a different `keychainService` from the one used to write. - Wiping only the token and leaving cached account data readable. - Assuming uninstall is a logout on iOS.
- Why revoke on the server before deleting the local copy?Revocation needs the refresh token in the request, so it has to happen while the app still holds it. It also covers copies you cannot delete, such as one extracted earlier. If the call fails offline, still wipe locally and rely on the token's expiry.
- Does deleting a biometric-protected item in expo-secure-store prompt the user?No. `deleteItemAsync` removes the item, including one written with `requireAuthentication`, without asking for biometrics, so logout and the first-launch wipe run silently.
saying these in an interview costs you the question
- Uninstalling an iOS app always deletes its Keychain items.
- Setting the stored token to an empty string is a proper logout.
- Resetting navigation to the sign-in screen is enough for logout.
- The install marker must go in secure storage to be trusted.
- Android restores a working token after reinstall like iOS can.