Token Refresh

Refreshing Tokens and Protecting Routes

Any API call can meet a 401 from an expired access token. BookNest wraps fetch(): it sends the saved access token, and on a 401 trades the refresh token for a new pair with refreshAsync() and retries once. The server rotates refresh tokens (each works once), so requests that fail together must share one refresh: refreshOnce() keeps the pending promise in a module variable and hands it to every caller until it settles; on failure it forgets both tokens and resolves null:

src/api/auth.ts: fetch with the access token, refreshed once on a 401 (excerpt)TypeScript
export async function authFetch(path: string, init: RequestInit = {}) {
  const send = (t: Tokens | null) => fetch(`${issuer}${path}`, {
    ...init, headers: { ...(init.headers as object), authorization: `Bearer ${t?.accessToken}` },
  });
  let res = await send(await readTokens());
  if (res.status === 401) {
    const fresh = await refreshOnce();
    if (!fresh) {
      signedOut();                                    // back to the sign-in screen
      return res;
    }
    res = await send(fresh);
  }
  return res;
}

To test it, the practice API has a hook, POST /expire-tokens, that ages every access token. After calling it, Place order on the checkout logged a refresh on the phone and three requests at the server:

Output of 203
I ReactNativeJS: /orders: 401, refreshing
18:57:22 POST /orders 401
18:57:23 POST /token 200
18:57:24 POST /orders 201

The user saw only "Order 1001 placed". After restarting the API, which forgets every token, the refresh got POST /token 400 (refresh failed: The provided authorization grant ... is invalid) and the app returned to /cart.

signedOut() is a listener the session registers to set the user to null. The Stack.Protected guard={user !== null} from Auth Flows then removes checkout and its history, whether the user signed out or the refresh token died. The real protection is the server, which now rejects POST /orders without a valid token.