ESC

Type to search the knowledge base.

Auth Session Design for SPAs

Frontend auth/session design for SPAs — tokens, cookies, refresh, route guards, XSS/CSRF tradeoffs, and UX.

intermediate4 min read
  • system-design
  • interview
  • architecture
  • auth
  • security

Scope the problem

In scope (frontend + BFF boundary):

  • Login/logout UX and session bootstrap
  • Where credentials live (memory vs cookie vs localStorage)
  • Access token refresh
  • Protected routes and API client integration
  • XSS/CSRF implications

Out of scope unless asked: OIDC provider internals, password hashing, full IAM org design.

Assumptions: SPA + HTTPS API; optional Backend-for-Frontend (BFF); OAuth2/OIDC common.

Requirements

Type Examples
Functional login, logout, session restore, step-up if needed
Security mitigate XSS token theft, CSRF on cookie sessions
UX no surprise logouts mid-flow; clear re-auth
Perf session check doesn’t block all static assets

Session models (pick deliberately)

Browser  —cookie→  BFF  —server session/token→  APIs
  • JS cannot read session cookie → better XSS posture for token theft
  • Needs CSRF protection (SameSite, CSRF token, or double-submit)
  • Natural fit with SSR/BFF
  • Access token only in JS memory (lost on refresh → silent refresh)
  • Refresh token in HttpOnly cookie to auth domain
  • Common OIDC SPA pattern with PKCE

3) Tokens in localStorage (discouraged)

  • Trivial XSS → full account takeover
  • Mention only to reject for sensitive apps

Interview recommendation: BFF cookie session or memory AT + HttpOnly refresh; never “localStorage JWT” as the hero design for banks/health.

High-level SPA architecture

┌─────────────────────────────────────────────┐
│ AuthProvider                                │
│  status: anonymous | loading | authenticated│
│  user profile cache                         │
├──────────────┬──────────────────────────────┤
│ Route guards │ API client (fetch wrapper)   │
│              │  - attach credentials        │
│              │  - 401 → refresh → retry     │
│              │  - queue concurrent refresh  │
└──────────────┴──────────────────────────────┘

Bootstrap sequence

App load
  → GET /session (or /oauth/userinfo via BFF)
  → if 200: AuthProvider = authenticated + profile
  → if 401: anonymous
  → render routes

Avoid flashing protected content: don’t render secret routes until status !== loading.

Protected routes

function RequireAuth({ children }: { children: React.ReactNode }) {
  const { status } = useAuth();
  const loc = useLocation();
  if (status === "loading") return <ShellSkeleton />;
  if (status === "anonymous") {
    return <Navigate to="/login" state={{ from: loc }} replace />;
  }
  return children;
}

Refresh design

Concern Approach
Expiry short-lived access token (5–15m)
Refresh single-flight promise; parallel 401s await it
Failure clear session; redirect login with return URL
Rotation refresh token rotation server-side
let refreshPromise: Promise<void> | null = null;

async function api(input: RequestInfo, init?: RequestInit) {
  const res = await fetch(input, { ...init, credentials: "include" });
  if (res.status !== 401) return res;
  refreshPromise ??= doRefresh().finally(() => {
    refreshPromise = null;
  });
  try {
    await refreshPromise;
  } catch {
    logoutLocal();
    throw new Error("unauthenticated");
  }
  return fetch(input, { ...init, credentials: "include" });
}

CSRF vs XSS tradeoff table

Storage XSS steals session? CSRF risk
HttpOnly cookie Harder (not JS-readable) Yes — need defenses
Memory access token Only while page alive; XSS can still call APIs as user Lower for classic CSRF
localStorage token Easy Lower CSRF; terrible XSS

Defense-in-depth: CSP, strict dependency hygiene, sanitize HTML, SameSite cookies, CSRF tokens for state-changing cookie auth.

Logout

  1. Client clear memory state
  2. Server revoke refresh/session
  3. Optional IdP logout
  4. Navigate to public page

Cross-tab: BroadcastChannel or storage event to sync logout.

Caching & performance

  • Profile in memory (React Query) with short stale time
  • Don’t cache authenticated HTML on shared CDN without Vary/private
  • Prefetch post-login route chunks
  • Silent refresh scheduled before expiry (not only on 401) to reduce failed calls

Edge cases

  • Clock skew on JWT exp
  • Multiple tabs refreshing simultaneously
  • Mid-checkout session expiry → preserve draft, re-auth modal
  • Third-party cookie blocking (if using cross-site IdP iframes) — prefer top-level redirects

Tradeoffs

  1. BFF vs pure SPA token — security vs infra complexity
  2. Long sessions vs risk — refresh rotation, device management
  3. Global logout vs UX friction
  4. SSR user-specific pages vs CDN caching

Interview close

State threat model first (XSS primary for SPAs). Choose cookie BFF or memory+refresh. Describe bootstrap, single-flight refresh, route guards, CSRF if cookies. End with logout multi-tab and CSP as complementary controls.

Further reading