The cookie problem in one sentence
You want your frontend to know who the user is, but you do not want a script that sneaks onto your page to be able to steal that user’s session.
Those two goals fight each other. Solving them for real takes more than the “just use JWT in localStorage” advice that filled blogs five years ago.
Why localStorage was always a bad idea
If you put a token in localStorage, any script that runs on your page can read it. That includes scripts you do not control, like an analytics tag, a third party widget, or a comment field that did not escape user input properly.
The moment any of those scripts is compromised, the attacker reads the token and uses it to impersonate the user from anywhere in the world. There is nothing the user can do, because they did nothing wrong.
This is the entire reason browsers invented cookies with the HttpOnly flag in the first place. A HttpOnly cookie is sent automatically with every request to your domain, but JavaScript on the page cannot read it. If a malicious script gets in, it cannot steal the cookie. The session leak surface shrinks dramatically.
The HttpOnly cookie + readable sidecar pattern
In 2026, the right pattern looks like this:
- The session token (the long lived JWT or a session id) lives in a
HttpOnly,Secure,SameSite=Laxcookie. JavaScript cannot touch it. - A short, non sensitive sidecar holds the things your UI needs to render: the user’s name, role, a short id. This sidecar lives in a regular cookie or in
localStorage. It is signed but not secret. - Your backend reads the
HttpOnlycookie on every request and decides what to do.
The frontend never has the real token. It only has enough to know “this user is called Maria, she is an admin, the session is still alive”. If the page is compromised, the attacker gets Maria’s name, not Maria’s account.
When you need to see what the sidecar contains, paste it into the JWT decoder on AldeaCode. Everything runs in your browser, the sidecar never leaves your tab.
CSRF: the cost of automatic cookies
The price you pay for using cookies is that browsers send them automatically. If your bank’s cookie is sent on every request to bank.com, then a malicious site can trick the browser into making a request to bank.com and the cookie comes along for the ride. That is cross site request forgery (CSRF).
The fix is two layers, both required:
SameSite=Lax(orStrict) on the cookie. The browser refuses to send the cookie on cross site requests for non navigation methods. This blocks 95 percent of CSRF without you doing anything else.- A CSRF token that the backend issues, the frontend includes in every state changing request, and the backend verifies. This catches the rest.
SameSite=Lax is the default in modern browsers, but you should set it explicitly so it survives a framework upgrade that decides differently.
XSS: still the bigger threat
Cross site scripting is still the way most sessions get stolen. An attacker injects script into your page, the script does whatever your page can do.
Even with HttpOnly cookies, an XSS lets the attacker do whatever the user is logged in to do, by making requests from the user’s own browser. They cannot read the session, but they can use it.
The defence is layered. First, sanitize and escape every piece of user input. The HTML stripper on AldeaCode helps when you need to flatten user input quickly. Second, ship a Content Security Policy that limits which scripts can run. Third, never trust your own templating, double check what makes it onto the page.
A practical 2026 setup
Backend issues two cookies on login:
session_token: HttpOnly, Secure, SameSite=Lax, expiry 1 hour to 7 days depending on what you tolerate.user_profile_jwt: regular cookie or localStorage, contains non sensitive display data, signed by the backend, short lived.
Backend rotates session_token on every refresh, ideally with a sliding window. Frontend reads user_profile_jwt to render names and roles, never relies on it for security decisions. CSRF token issued and required for any non GET request.
When you need to inspect what your backend is putting in the sidecar, the JWT decoder and the base64 encoder work entirely in your browser. The hash generator is useful when you need to confirm the cookie contents have not changed shape between deploys.
The whole approach assumes your backend is the source of truth. The frontend is a presentation layer that gets enough information to look right, never enough to be the security boundary. That is the only way to keep XSS and CSRF from turning a small bug into an account takeover.