Auth Session Design for SPAs
Frontend auth/session design for SPAs — tokens, cookies, refresh, route guards, XSS/CSRF tradeoffs, and UX.
- 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)
1) HttpOnly secure cookie session (BFF)
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
2) SPA with access token in memory + refresh cookie
- 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
- Client clear memory state
- Server revoke refresh/session
- Optional IdP logout
- 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
- BFF vs pure SPA token — security vs infra complexity
- Long sessions vs risk — refresh rotation, device management
- Global logout vs UX friction
- 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.
Related on this site
- Security Interview Talking Points
- Client-side Routing at Scale
- Feature Flags Client Architecture
- System Design Interview Framework