Table of Contents
For years, the default Angular auth tutorial looked like this: call /login, get a JWT, stash it in localStorage, attach Authorization: Bearer … from an interceptor, and sprinkle a couple of route guards on top. It ships. It demos well. And it quietly trains teams to treat the browser as a safe place for long-lived credentials.
It is not.
In 2026 the guidance has converged, and it is blunt:
- Prefer OAuth 2.0 / OIDC with a reverse proxy or dedicated auth provider — not a hand-rolled password form that mints forever-tokens for the SPA.
- Prefer short-lived access tokens (and refresh handled outside the SPA) over long-lived credentials in the browser.
- Prefer HttpOnly, Secure, SameSite cookies over storing sensitive JWTs in
localStorageorsessionStorage.
This post is the practical shape of that advice for Angular apps: why localStorage JWTs lose to XSS, how a Backend-for-Frontend (BFF) or reverse-proxy session keeps tokens off the client, and what Angular still owns — interceptors, CSRF headers, and guards that improve UX without pretending to enforce authorization.
The Short Version
If JavaScript can read your access token, so can an XSS payload.
That is the whole argument against browser-visible JWTs. An HttpOnly session cookie cannot be exfiltrated with localStorage.getItem('token'). A BFF (or reverse proxy acting as one) can run the OIDC authorization code flow with PKCE, hold access and refresh tokens server-side, and give the browser only an opaque session cookie. Angular then calls same-origin APIs with credentials. Tokens never appear in application code.
Route guards and interceptors still matter — for navigation, refresh UX, and attaching CSRF headers — but authorization is enforced on the server. Always.
Why “Just Add JWT” Failed in the Browser
The classic SPA pattern looks secure because JWTs feel cryptographic. The failure mode is not signature verification. It is token exfiltration.
XSS turns storage into theft
Any script that runs in your origin can read localStorage and sessionStorage. That includes:
- a compromised npm dependency that runs at runtime,
- a stored XSS in markdown, comments, or rich text your app renders,
- a framework sanitizer bug (see the recent Angular SVG/MathML advisory wave in Why Client-Side Frameworks Need Security Updates),
- a malicious browser extension with page access (less common in threat models, still unpleasant).
Once the payload has the JWT, it can call your APIs from anywhere until the token expires. If that token is long-lived, the blast radius is measured in days or weeks, not minutes.
HttpOnly cookies change the game. Malicious script can still act as the user in the current browser session (that is XSS), but it cannot casually copy a bearer token into an attacker-controlled environment and replay it later from a laptop across the world. Session fixation, CSRF, and session lifetime become the problems you design for — and those are well-understood cookie problems, not “hope our XSS never happens” problems.
Bearer tokens in JS also fight the platform
When Angular attaches Authorization from memory or storage, every request becomes a custom credential flow. Cookies, by contrast, are same-site aware, participate in browser privacy rules, and pair naturally with Angular’s built-in XSRF support when you stay same-origin.
The IETF work on OAuth 2.0 for Browser-Based Applications has been pushing the industry the same direction: treat the browser as a public client that should not hold refresh tokens if you can avoid it, and prefer a backend component that does.
The Modern Shape: OIDC + BFF (or Reverse Proxy)
Think in three layers:
| |
What the BFF does
- Starts the OIDC authorization code flow with PKCE.
- Exchanges the code for tokens.
- Stores tokens in a server-side session (Redis, encrypted server cookie store, etc.).
- Sets a browser cookie that is at least:
HttpOnly— not readable from JavaScript,Secure— HTTPS only in production,SameSite=LaxorStrict— limits cross-site sending.
- On each API call from Angular, loads the session, refreshes the access token if needed, and calls downstream services.
Managed IdPs and API gateways often package this as a documented “BFF” or “token handler” pattern. You do not have to invent cryptography. You do have to stop shipping the access token into the SPA bundle’s runtime storage.
What Angular does not do anymore
- No
localStorage.setItem('access_token', …). - No silent iframe renew that parks refresh tokens in the browser.
- No interceptor whose main job is
setHeaders: { Authorization: \Bearer ${token}` }` for first-party APIs.
Those patterns still appear in older SPA OIDC libraries when the SPA is the OAuth client. They are a deliberate tradeoff. For first-party Angular apps talking to your own APIs, the BFF tradeoff is usually the better default in 2026.
Angular Still Has Real Work
Removing tokens from the browser does not remove frontend auth code. It changes what that code is responsible for.
1. Call APIs with credentials
Same-origin requests should include cookies:
| |
Or set withCredentials once in an interceptor so individual calls cannot forget.
2. CSRF for cookie sessions
Cookies mean CSRF is back on the checklist. SameSite helps a lot; it is not a complete substitute for defense in depth on state-changing requests.
A common pattern:
- BFF sets a readable CSRF cookie (not HttpOnly).
- Angular sends the value back as
X-CSRF-TokenonPOST/PUT/PATCH/DELETE. - BFF rejects mismatches.
Angular’s own HttpClient XSRF support covers the classic cookie-to-header pattern for same-origin calls when configured for your cookie and header names. The companion uses an explicit interceptor so the flow is obvious when teaching.
| |
3. Interceptor for 401 → refresh → retry
Access windows should be short. When the BFF says the access session expired, Angular should ask the BFF to refresh — not dig a refresh token out of storage.
| |
Register both interceptors with provideHttpClient(withInterceptors([csrfInterceptor, authInterceptor])).
Notice what is missing: there is no bearer token variable. Refresh is an HTTP call to your BFF. The new access token stays on the server.
4. Auth and role guards (UX only)
Functional guards keep users out of screens they should not see. They are not a security control.
| |
Wire them on routes:
| |
Then enforce the same rule on /api/admin. If someone bypasses the router (devtools, old PWA shell, crafted fetch), the API must still return 403. Guards hide buttons and prevent awkward flashes of forbidden UI. Servers stop unauthorized work.
Migrating Off localStorage JWTs
A pragmatic path for an existing Angular app:
- Introduce a BFF or gateway in front of your APIs (or enable your IdP’s official BFF / token-handler mode). Keep the SPA on the same site as the BFF so cookies are first-party.
- Move login from “POST credentials → JWT in JSON” to “redirect to IdP → BFF sets session cookie.”
- Delete token storage in the SPA. Search for
localStorage,sessionStorage, andAuthorizationbuilders. Remove them for first-party calls. - Switch HttpClient to
withCredentials: trueand add CSRF handling. - Shorten access lifetime on the server. Implement BFF refresh. Point your 401 interceptor at that endpoint.
- Keep guards, but audit every sensitive API for server-side authz.
- Rotate any tokens that ever lived in browser storage — treat them as potentially leaked.
You can migrate route-by-route. The hard part is usually cookie domains, CORS, and deciding that the SPA is no longer an OAuth client. Once that decision is made, Angular code gets simpler.
What the Companion Demonstrates
The angular-modern-auth example is intentionally small:
- a teaching BFF that issues HttpOnly
sidcookies and a readable CSRF cookie, - login that never returns access/refresh tokens to the browser,
- short access TTL so you can watch refresh happen,
- Angular interceptors and guards matching the snippets above,
/api/profileand/api/adminthat enforce session and roles on the server.
Run it locally (npm start in that folder), sign in, and inspect DevTools → Application. You should see cookies — and an empty token shelf in Local Storage. That empty shelf is the point.
Final Thought
“Just add JWT” was a convenient story for SPAs when everyone wanted a pure static frontend and a stateless API. The threat model did not age well. XSS is still with us. Supply-chain script is still with us. Framework sanitizers still ship CVEs.
Modern Angular auth is less about decorating HttpClient with bearer tokens and more about keeping credentials out of the JavaScript heap. Use OIDC. Put a BFF or reverse proxy in front. Prefer short-lived server-side tokens and HttpOnly cookies. Keep interceptors and guards for the UX and CSRF plumbing they are good at — and let the server remain the only component allowed to say “yes.”