Skip to main content
ailita-library manages two tokens: a short-lived accessToken held in memory, and a refreshToken stored in the ailita_rt browser cookie. Understanding where each token lives — and the deliberate limitations of JS-set cookies — is essential for a correct threat model when integrating this library into a production application. This page covers:
  • Why accessToken lives only in JavaScript memory and never in localStorage
  • The ailita_rt cookie: what attributes it has, and the explicit disclosure that it is NOT httpOnly
  • The 401 refresh queue: how concurrent requests are parked and replayed after a single refresh attempt
  • The security boundary table: what the library handles and what your backend or infrastructure must own

Access token — in-memory only

The accessToken is stored in a module-level variable (_accessToken) inside src/api/client.ts. It is never written to localStorage, sessionStorage, or any other persistent browser storage. The variable is scoped to the module closure and is not exposed on window.
The token is set via setTokens() after a successful login or token refresh, and cleared via clearTokens() on logout or refresh failure. No browser storage API is involved.
Because the access token is in memory, it is lost on page refresh. This is intentional — the library silently re-acquires a new access token from the refresh token cookie on every AuthProvider mount (via POST /auth/refresh). The trade-off is a single network round-trip on every page load versus the XSS exposure of persisting the token in browser storage.

Why not localStorage?

An access token in localStorage is readable by any JavaScript executing in the same origin — including third-party scripts, injected ads, and XSS payloads. An in-memory token cannot be extracted by XSS code that does not have direct access to the module closure. ailita-library uses the in-memory pattern to limit the blast radius of a script injection attack to the current page session only. Consider the comparison:
The in-memory pattern does not protect against a persistent XSS attack that intercepts the token in-flight (e.g., by monkey-patching fetch or Axios before the library initializes). It only prevents token exfiltration from persistent storage after the fact. Defense-in-depth requires Content Security Policy headers — see Login Spoofing Prevention for CSP guidance.
ailita_rt is set via document.cookie from client-side JavaScript. It is NOT an httpOnly cookie. The cookie is therefore readable by any JavaScript executing on the page — including XSS payloads — which makes it a higher-value attack target than the in-memory access token. Understand this before deploying to production.
The httpOnly attribute can only be set by the server in a Set-Cookie response header. The ailita-library client sets ailita_rt from JavaScript after a successful login response — this is a deliberate architectural choice that allows the library to work without requiring the backend to return a Set-Cookie header. The exact cookie string written is:
The full implementation from src/api/client.ts:
The token value is URL-encoded before storage and decoded when read back via decodeURIComponent.

The httpOnly upgrade path

If your backend team can return ailita_rt as a Set-Cookie: ailita_rt=...; HttpOnly; SameSite=Lax response header, that upgrade eliminates the JS-readable cookie risk. The library reads ailita_rt from document.cookie on the client — switching to httpOnly requires verifying that POST /auth/refresh still works when the cookie is sent automatically by the browser rather than explicitly by the library. Verify the upgrade path with your backend team before adopting it. Do not treat this as a drop-in change.
The reason the httpOnly upgrade is non-trivial: the library currently reads ailita_rt explicitly from document.cookie and passes it in the POST /auth/refresh request body:
If the backend switches to httpOnly, it must accept the cookie sent automatically via the request’s Cookie header rather than a body field. This is a backend contract change that requires coordinated testing before production rollout.

401 refresh queue

When a request returns a 401, the Axios response interceptor in client.ts attempts a silent token refresh. The queue mechanism ensures that if multiple requests fire concurrently and all receive a 401, only one refresh attempt is made — not one per failing request. The two key state variables that drive the queue are:

Step-by-step queue behavior

Step 1 — First 401 arrives: _isRefreshing is false. The interceptor sets _isRefreshing = true, marks the original request with _retry = true, and calls POST /auth/refresh with the current ailita_rt cookie value. Step 2 — Concurrent 401s (while refresh is in-flight): Each additional request that returns 401 sees _isRefreshing = true and pushes a { resolve, reject } pair onto _refreshQueue — effectively parking those requests. They are converted into pending Promises that will settle only when the refresh completes. Step 3 — Refresh succeeds: The new accessToken is written to memory via setTokens() and ailita_rt is updated in the cookie. All queued resolve callbacks fire with the new token. Each parked request injects the updated Authorization: Bearer <newToken> header and retries via apiClient(originalRequest). _isRefreshing resets to false in the finally block. Step 4 — Refresh fails (no valid refresh token, server error, or token expired): All queued reject callbacks fire, causing each parked request to reject with the refresh error. clearTokens() wipes the in-memory access token and removes the ailita_rt cookie. _onSessionExpired?.() is called — this is your signal to redirect the user to the login page. _isRefreshing resets to false in the finally block.

The hadAuthHeader guard

Only requests that were sent with an Authorization: Bearer header are eligible for the refresh queue. Public endpoints (login, signup, forgot-password) do NOT trigger a refresh attempt even if they return 401 — this prevents an infinite loop where a failed login attempt incorrectly fires onSessionExpired.The guard in client.ts checks:
If hadAuthHeader is false, the interceptor passes the 401 error through to the caller without attempting any refresh. This means a bad password on the login form returns 401 cleanly to LoginForm’s error handler rather than triggering onSessionExpired.
onSessionExpired is called once per refresh failure — not once per queued request. If 5 requests were queued and the refresh fails, all 5 reject immediately but onSessionExpired fires exactly once. Your handler must be idempotent (safe to call multiple times if it fires during a race in your app’s routing layer). See Session Management for the full onSessionExpired contract, including what state has already been cleared when your callback runs.

Full interceptor flow diagram

Security boundary

ailita-library is a client-side library. The following table is an explicit contract of what it handles and what it does not. Use this as a checklist when reviewing your production security posture — every “No” row is a gap your backend or infrastructure must fill independently.
The Secure cookie attribute is also not set by the library — cookies without Secure are transmitted over plain HTTP. This is safe only if you enforce HTTPS at the infrastructure layer. In development over http://localhost, omitting Secure is standard behavior and expected by browsers.

What this means in practice

CSRF protection: The SameSite=Lax cookie attribute provides partial CSRF protection — it prevents the cookie from being sent on cross-origin sub-resource requests (images, iframes, XHR/fetch initiated by third-party pages). It does NOT prevent CSRF on same-site requests or when users navigate directly to URLs. Full CSRF protection requires a server-side token that the backend validates on state-changing requests. Rate limiting: ailita-library makes no attempt to throttle login attempts. If you allow unrestricted POST /auth/login calls, automated credential-stuffing attacks will reach your backend unimpeded. Backend-side rate limiting (e.g., per IP, per email, with progressive backoff) is required independently. Token revocation: When a user calls logout(), the library clears local tokens but only fires the request to DELETE /auth/logout to invalidate the server-side session. If that request fails (network error, server down), the local state is cleared but the server-side refresh token may remain valid until it expires naturally. A robust logout implementation should handle this edge case on the server side.

Next steps

Login Spoofing Prevention

iframe embedding, phishing domain protection, CSP headers, and clientId/Origin validation

Session Management

onSessionExpired contract, useSessions, and session revocation