@liam-public/browser-react-auth
v0.4.0
Published
React browser auth client, provider, JWT helpers, and authenticated fetch.
Downloads
184
Readme
@liam-public/browser-react-auth
Browser session store, React provider, JWT helpers, and an authenticated fetch. Framework-side
half of a token auth flow — it holds and renews a session, and leaves how you obtain one (OIDC,
password, passkey) to the caller's refresh callback.
import {
createBrowserAuthClient,
createWebStorageTokenStore,
createAuthenticatedFetch,
AuthProvider,
useAuth,
} from '@liam-public/browser-react-auth'
const client = createBrowserAuthClient({
storage: createWebStorageTokenStore(window.localStorage),
refresh: async () => exchangeRefreshToken(), // → session, or null when it is refused
})
<AuthProvider client={client}>
<App />
</AuthProvider>Exports
createBrowserAuthClient(options)— the session store: bootstrap, get/set/clear, refresh, subscribe,isAuthenticated().createWebStorageTokenStore(storage)/createMemoryTokenStore()—BrowserTokenStoreimplementations overlocalStorage/sessionStorageand aMap.createAuthenticatedFetch(client, fetch?)— bearer injection with one refresh-and-retry on 401.AuthProvider/useAuth()— React binding;useAuth()throws outside a provider.decodeJwtPayload(token)/isJwtExpired(token, now?)— claim helpers. Decoding only, no signature verification: treat the payload as a hint for UI, never as an authorization decision.
Bearer only, no cookies
createAuthenticatedFetch sends credentials: 'omit' on both the first attempt and the retry,
and that overrides whatever the caller passes. The bearer token is the only credential this
client sends. A cookie riding along would be an ambient second credential the server might honour
instead of the token — the confused-deputy shape bearer auth exists to avoid.
Concurrent 401s share a single refresh; the request is retried exactly once. Request bodies must be re-readable for that retry, so a stream body cannot be retried.
Storage keys
Keys default to a liam_browser_auth_ prefix. Two ways to change them:
createBrowserAuthClient({ …, keyPrefix: '' }) // bare: access_token, refresh_token, user
createBrowserAuthClient({ …, keys: { refreshToken: 'my_rt' } }) // name keys outright; wins over keyPrefixUse keys when another component shares the same storage and must agree on a name — an OIDC
engine that renews from refresh_token, say. Two components writing one token under two names
drift apart the moment either clears its copy alone, which leaves a usable credential behind
after sign-out.
Refresh on load, not on a timer
bootstrap() restores the session from storage. If the stored access token has expired and a
refresh token survives, it renews once before resolving — that is what stops a reload from
bouncing a signed-in user to the login screen. Concurrent bootstrap() calls share one restore.
A refresh the server refuses (returns null) clears the session. A refresh that throws
(offline) is swallowed and leaves the refresh token in place, so a later attempt can still
succeed — a network blip should not destroy credentials.
There is no proactive background refresh: renewal happens on load and on a 401. AuthProvider
awaits bootstrap() before clearing isLoading, so route guards never observe the expired window.
