osbytes

Search

Find posts, projects, and members.

← back to blog
2026-05-10·5 min read·#healthcare #fhir #oauth #javascript #browsers #realtime #open-source

When many tabs meet rotating refresh tokens — coordinating OAuth refresh across the browser

Clinical and operations workflows rarely stay inside one browser tab. People keep patient charts, schedules, messaging, reference material, and admin views open side by side, often on the same origin or the same SPA session. Many healthcare products also lean on live updates: FHIR Subscriptions, polling, or background refetches that fire around the same time in every visible tab.

That combination matters for authentication. Short-lived access tokens are normal; refreshing them is normal too. Where it gets subtle is refresh-token rotation: each successful refresh invalidates the previous refresh credential on the server. That is a solid security trade-off, but the browser is not a single process. Each tab has its own JavaScript heap, while localStorage and other origin-wide persistence (not tab-scoped sessionStorage) are visible across tabs. In-memory deduplication inside one tab does not coordinate with its neighbors.

The generalized failure mode

Picture two tabs that both decide they need a refresh at nearly the same instant, which is easy when access tokens are about to expire and both tabs have realtime-driven traffic. Each tab starts an OAuth token exchange with what is still the same rotated refresh handle in memory or storage. One request succeeds and the server mints a new refresh secret; the other arrives a moment later with the old secret and fails.

What happens next depends on the client, but a common pattern is harsh: treat the failed refresh as “logged out,” clear shared storage, and notify the app. The tab that lost the race just wiped the session for every tab, including the one that had already refreshed successfully. Users experience intermittent mid-task sign-out (timing- and tab-count-sensitive, so hard to reproduce), which is frustrating in any domain and especially disruptive when someone is mid-documentation or mid-order entry.

A browser-native way to serialize the critical section

The fix is conceptually small: only one tab should perform the refresh network round-trip at a time for a given session namespace; everyone else should wait, then re-read shared storage and skip the HTTP call if a peer already persisted new tokens.

The Web Locks API is a good fit. Tabs on the same origin can contend on a named lock (scoped so unrelated apps on the same host do not block each other). One tab holds the lock while it completes the token exchange and writes the new session; the next tab acquires the lock, loads the updated session from storage, and avoids issuing a redundant refresh if nothing is stale anymore. Locks are released automatically if a tab closes mid-flight, which avoids a common deadlock worry without inventing new timeouts everywhere.

Where navigator.locks is missing (some workers, older browsers, server-side runtimes), clients typically fall back to whatever coordination they already had, usually in-tab deduplication only, so behavior degrades to the pre-fix story rather than breaking builds.

This pattern is not tied to one framework: any SPA that stores OAuth session in shared storage and talks to a rotation-aware authorization server can hit the same race under multi-tab + bursty traffic.

Where this landed for us

Medplum is an open-source, FHIR-native healthcare platform; its audience naturally runs many concurrent tabs, and FHIR Subscription–driven clients increase the odds that refreshes line up across windows. A concrete implementation of the Web Lock approach merged in medplum/medplum#9113; Medplum-specific client details, testing notes, and edge cases live there for anyone integrating or debugging against their stack.

The broader takeaway is simpler: treat cross-tab OAuth refresh as a concurrent systems problem, not just an in-tab async queue, especially when your users live in a handful of tabs at once and your UI keeps them all warm at the same time.