← Back to the library
MobileDeep dive · 9 min

Users get logged out at random

“Support hears "it logged me out again" a few times a day. It clusters around app resume and screens that fire several requests at once, and we can never reproduce it on Wi-Fi.”

THE PRINCIPLE

Refresh is a critical section. One in-flight refresh per session, shared by every caller, with the new tokens stored before anyone uses them.

FIRST MOVES

  1. Correlate logouts with auth-server logs: look for reuse or invalid_grant on refresh (Auth0 logs these as ferrt) right after two refreshes with the same token, or a refresh whose response the client never received.
  2. Wrap refresh in a single-flight promise shared by all requests, queue other 401s behind it, and retry each original request once with the new access token.
  3. Persist the new refresh token before resolving the promise; a crash or kill between receiving and storing it leaves the device holding a revoked token.
  4. Treat only explicit rejections (400 invalid_grant, a revoked session) as logout. Timeouts, 5xx and offline are retryable; keep the session and back off.
  5. Check storage: on iOS, SecureStore/Keychain items default to WHEN_UNLOCKED, so a background task refreshing while the phone is locked can't read or write them; use AFTER_FIRST_UNLOCK for tokens that background work needs.

TOOLS: THEN → NOW

Long-lived refresh tokens in AsyncStorageRotating refresh tokens in Keychain/Keystore (expo-secure-store, react-native-keychain)
Each request refreshes on its own 401A single-flight refresh in the HTTP client, plus a short server-side reuse window (for example Auth0 leeway)
Bearer refresh tokens for public clientsRotation or sender-constrained tokens (DPoP), as RFC 9700 requires

PATTERN SNAPSHOT

let inFlight: Promise<Tokens> | null = null;

// Every caller that hits a 401 awaits the same refresh.
function refreshOnce(): Promise<Tokens> {
  inFlight ??= (async () => {
    const current = await tokenStore.read();
    const next = await authApi.refresh(current.refreshToken); // throws AuthRejected on invalid_grant
    await tokenStore.write(next); // persist before anyone uses it
    return next;
  })().finally(() => { inFlight = null; });
  return inFlight;
}

CLOSE THE AI. EXPLAIN THIS.

Two requests get a 401 at the same moment and both call refresh with the same rotating token. What does the authorization server do, and what does the user see?

HOW IT WORKS UNDERNEATH

SOURCES

Guide reviewed