skip to content

In expo-secure-store, what does the iOS keychainAccessible option control, and which value suits a token read by background refresh?

level: middleimportance: should knowfreq 28%

answer

  1. when the item can be read
  2. default WHEN_UNLOCKED
  3. locked device: interaction not allowed
  4. AFTER_FIRST_UNLOCK for background
  5. THIS_DEVICE_ONLY skips backups

basics

~20 s

keychainAccessible sets the iOS Keychain accessibility of the stored item, meaning when it can be read. The default WHEN_UNLOCKED fails while the device is locked, so a token read by background refresh needs AFTER_FIRST_UNLOCK or its THIS_DEVICE_ONLY variant.

solid answer

~40 s

`keychainAccessible` maps to the item's `kSecAttrAccessible` attribute and is iOS-only; Android ignores it. The default, `SecureStore.WHEN_UNLOCKED`, lets the item be read only while the device is unlocked, so a background refresh of price alerts running on a locked phone gets a Keychain error (`User interaction is not allowed.`) and the request goes out without a token. `AFTER_FIRST_UNLOCK` makes it readable after the first unlock since boot, which is what background work needs. Each has a `_THIS_DEVICE_ONLY` variant that is not migrated to a new device from a backup, and `WHEN_PASSCODE_SET_THIS_DEVICE_ONLY` requires a passcode and deletes the item if it is removed. `ALWAYS` and `ALWAYS_THIS_DEVICE_ONLY` are deprecated. The attribute is applied when the item is created; updating an existing value keeps its old accessibility, so delete and re-save to change it.

code

typescript · 14 lines
typescript
import * as SecureStore from 'expo-secure-store';

const TOKEN_KEY = 'session_token';
const OPTIONS: SecureStore.SecureStoreOptions = {
  keychainAccessible: SecureStore.AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY,
};

// One-off migration of a token saved by an older release with the default WHEN_UNLOCKED.
export async function migrateTokenAccessibility(): Promise<void> {
  const token = await SecureStore.getItemAsync(TOKEN_KEY); // run while in the foreground
  if (token === null) return;
  await SecureStore.deleteItemAsync(TOKEN_KEY);
  await SecureStore.setItemAsync(TOKEN_KEY, token, OPTIONS);
}

go deeper

for a junior

Recall that keychainAccessible is iOS-only, defaults to WHEN_UNLOCKED, and decides when the stored value can be read.

for a middle

Explain why background reads fail on a locked device with the default and how AFTER_FIRST_UNLOCK and THIS_DEVICE_ONLY variants change that.

for a senior

Plan the change safely: existing items keep their accessibility, so migrate with read, delete and re-save, and handle reads before first unlock.

for a principal

Set a per-secret policy for lock-state access and device migration, balancing background features against exposure on a lost or restored device.

## What accessibility means on iOS Every iOS Keychain item carries an **accessibility** attribute (`kSecAttrAccessible`) that tells the system when the item's data may be decrypted and returned. It is tied to the device's lock state and to whether the item may travel to another device through a backup. `expo-secure-store` exposes it as the `keychainAccessible` option, taking one of the module's exported constants. It is marked iOS-only; the Android implementation has no such field and ignores it. ## The values in SDK 57 | Constant | Readable | Moves to a new device via backup | Notes | |---|---|---|---| | `WHEN_UNLOCKED` | only while unlocked | yes | the **default** | | `WHEN_UNLOCKED_THIS_DEVICE_ONLY` | only while unlocked | no | | | `AFTER_FIRST_UNLOCK` | after the first unlock since boot | yes | suits background work | | `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` | after the first unlock since boot | no | | | `WHEN_PASSCODE_SET_THIS_DEVICE_ONLY` | while unlocked, passcode required | no | item deleted if the passcode is removed | | `ALWAYS`, `ALWAYS_THIS_DEVICE_ONLY` | always | yes / no | **deprecated**: use an option with some protection | ## The background-refresh failure A crypto-price tracker refreshes prices and checks alert thresholds in a background task, sending the session token with the request. Everything works in testing, where the phone is usually unlocked. In the field, many background runs happen while the phone is locked in a pocket. With the default `WHEN_UNLOCKED`, the Keychain refuses to return the item then; iOS reports the status that expo-secure-store surfaces as the message `User interaction is not allowed.`, and `getItemAsync` rejects. The fix is to store the token with an accessibility that allows reads while locked: - `AFTER_FIRST_UNLOCK` allows reads after the user has unlocked the phone once since it booted, which covers background work in normal use. - `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY` does the same and also keeps the token from being restored onto a different device from a backup, which is usually right for session tokens: a new phone should sign in again. Reads still fail between a reboot and the first unlock, so background code should expect a rejection and retry later rather than treating it as a sign-out. ## A trap when changing it The accessibility is written when the item is **created**. In expo-secure-store's iOS implementation, saving a key that already exists turns into a Keychain update that replaces only the value data. So an app that switches from the default to `AFTER_FIRST_UNLOCK` in a new release, and calls `setItemAsync` with the new option on a token saved by the old release, still has an item with `WHEN_UNLOCKED`. To migrate: 1. Read the current value while the app is in the foreground. 2. `deleteItemAsync` the key. 3. `setItemAsync` it again with the new `keychainAccessible`. ## Choosing a value - **Foreground-only secrets**, such as a PIN-unlock key used only when the user is looking at the app: `WHEN_UNLOCKED_THIS_DEVICE_ONLY`. - **Tokens used by background tasks**: `AFTER_FIRST_UNLOCK_THIS_DEVICE_ONLY`. - **Secrets that must require a passcode**: `WHEN_PASSCODE_SET_THIS_DEVICE_ONLY`, accepting that removing the passcode deletes them. - **Never** the deprecated `ALWAYS` values for new code. ## Reading in the background safely Background code should treat a rejected read as "try later", not as "signed out". Deleting the session because a read failed while the phone was locked would sign the user out every night. Keep the sign-out path for a server response that rejects the token, and let the background task skip its run when the Keychain says no. ## Testing the choice - Lock the phone, trigger the background task, and confirm the request carries the token. - Reboot the phone and trigger the task before unlocking: with `AFTER_FIRST_UNLOCK` the read still fails, and the task should back off quietly. - Restore a backup onto a second device and confirm a `_THIS_DEVICE_ONLY` token is absent there. - Upgrade from the previous release and confirm the migration ran, since an unmigrated item keeps the old accessibility. Because this is an iOS attribute, Android behaviour is unaffected by the choice: there, the Keystore-encrypted value is readable by the app whenever the app is running, unless `requireAuthentication` is set.

  • Why prefer the THIS_DEVICE_ONLY variant for a session token?
    It keeps the item out of backups that restore onto a different device. A new phone restored from backup then starts signed out and signs in again, instead of carrying over a live token the server issued to the old device.
  • Does keychainAccessible change anything on Android?
    No. It is an iOS Keychain attribute, and expo-secure-store's Android options have no such field. On Android the Keystore-encrypted value is readable whenever the app runs, unless `requireAuthentication` gates it behind user authentication.

saying these in an interview costs you the question

  • The default WHEN_UNLOCKED lets background tasks read the token on a locked phone
  • keychainAccessible also controls Android Keystore behaviour
  • ALWAYS is the recommended choice for background access
  • Calling setItemAsync with a new keychainAccessible updates an existing item's accessibility
  • THIS_DEVICE_ONLY items are copied to a new phone from a backup