How do you handle authentication and authorization in frontend apps
Authentication = who you are (login, tokens/sessions); authorization = what you can do (roles/permissions). Store tokens safely (httpOnly cookies > localStorage), refresh them, guard routes, hide unauthorized UI — but the real enforcement is always server-side. The frontend only reflects auth state.
Two distinct concepts, and one rule that governs both: the frontend never enforces — it reflects.
Authentication vs Authorization
- Authentication — who are you? Login, verifying credentials, establishing a session.
- Authorization — what are you allowed to do? Roles, permissions, ownership checks.
Authentication on the frontend
Token storage — the key decision:
- httpOnly, Secure, SameSite cookies — preferred. JS can't read them, so they're immune to XSS token theft. The browser attaches them automatically. Pair with CSRF protection (SameSite + CSRF tokens).
- localStorage / memory — readable by JS, so any XSS leaks the token. If you must (e.g. token in an
Authorizationheader for a separate API), keep it in memory, not localStorage, and have a tight CSP.
Refresh flow — short-lived access token + longer-lived refresh token; transparently refresh on 401, log out when refresh fails.
Auth state in the app — a context/store holding the current user; an AuthProvider that hydrates it on load (from cookie session or a /me call).
Authorization on the frontend
- Route guards — a
<ProtectedRoute>that redirects unauthenticated users to login, and role-gated routes that redirect unauthorized users. - Conditional UI — hide/disable buttons and links the user can't use (
{user.canEdit && <EditButton/>}). - Per-permission rendering — a
<Can permission="...">component oruseHasPermission()hook.
The rule that matters most
All of the above is UX, not security. Hiding a button doesn't protect the action — anyone can open devtools, call the API directly, or tamper with client state. Every protected action and every piece of sensitive data must be authorized on the server. The frontend's job is to make the unauthorized paths invisible and pleasant to avoid — not to be the gate.
Other concerns
- HTTPS everywhere; never log tokens.
- Handle token expiry gracefully (refresh, or redirect to login preserving intended destination).
- Clear all auth state on logout; invalidate the session server-side.
- OAuth/OIDC/SSO via a provider rather than rolling your own.
The framing
"Authentication is who you are, authorization is what you can do. On the frontend: prefer httpOnly Secure cookies over localStorage so XSS can't steal the token, run a refresh-token flow, hold auth state in a context, guard routes, and conditionally render by permission. But the load-bearing point is that none of this is security — it's UX. The server authorizes every request; the frontend just reflects that state so users don't hit walls."
Follow-up questions
- •Why are httpOnly cookies safer than localStorage for tokens?
- •How does a refresh-token flow work?
- •Why isn't hiding a button real authorization?
- •How do you protect cookie-based auth from CSRF?
Common mistakes
- •Storing JWTs in localStorage where any XSS can steal them.
- •Treating hidden UI as a security boundary.
- •No token refresh, so sessions break abruptly.
- •Not clearing auth state / invalidating the session on logout.
- •Rolling custom auth instead of using a vetted OAuth/OIDC provider.
Performance considerations
- •Auth hydration on load can block the first render — show a splash/skeleton while resolving the session. Avoid a refresh-token request waterfall on every navigation by caching the access token in memory.
Edge cases
- •Token expires mid-session — refresh transparently or redirect preserving the destination.
- •User's permissions change server-side while they have the app open.
- •Multiple tabs — keep auth state in sync (storage events / BroadcastChannel).
- •Deep-linking to a protected route while logged out.
Real-world examples
- •Auth0/Clerk/NextAuth handling OAuth, sessions, and refresh out of the box.
- •A <ProtectedRoute> wrapper plus server-side checks on every API endpoint.