Misconceptions About JWTs, Cookies, and Session-Based Auth
June 08, 2025
I’ve lost count of how many posts I’ve read on X where someone claims that JWTs are the worst authentication method out there. Some even go as far as saying, “Using JWTs will get your users hacked.”
Personally, I like JWT-based authentication for many reasons. I appreciate how easily you can authenticate services / users without needing a central source of truth. I also like that you can embed extra information directly into the token. So, it’s a bit frustrating when people make such absolute statements.
In this article, I want to share my perspective on some of the most common misconceptions people have about JWTs and cookies. I’ll also briefly explain how I would approach implementing authentication.
Also, please keep in mind that I’m a junior developer sharing my personal experience and perspective on a topic I’ve seen often misunderstood. This article isn’t meant to be a definitive guide.
Why use localStorage?
For me, there are two main sources of misconceptions around JWTs. The first is the widespread convention of storing auth tokens in localStorage.
Let’s be clear: storing sensitive information in localStorage is, 99% of the time, a bad practice. Why? Because localStorage is accessible via JavaScript, making it vulnerable to XSS attacks, malicious browser extensions, and other advanced exploitation techniques. Unlike cookies, localStorage doesn't support flags like HttpOnly or SameSite, which are essential for mitigating these risks.
If a website has an XSS vulnerability and a malicious actor injects a data-stealing script, they can easily retrieve the JWT from localStorage and exfiltrate it to a third-party server. This is not the case with cookies—properly configured with HttpOnly and SameSite, cookies cannot be accessed via JavaScript or sent to unauthorized domains.
Another overlooked risk is that malicious browser extensions with content script permissions can access localStorage just like any script running on the page. This means a compromised or shady extension could silently read your tokens and leak them—something that’s not possible with cookies marked as HttpOnly.
So it’s fair to ask: why use localStorage at all? Mainly due to convention and size limitations. Cookies typically cap out at around 4 KiB, which can be restrictive for very large tokens. Just include strictly necessary information and that’s it. The default token shown in jwt.io measures around 0.17KiB which is not a lot.
You cannot ban a user / close a session when using JWTs
Now that we’ve addressed the cookies vs localStorage misconception, which I consider the easier one. Let’s move on to a slightly more complex issue that tends to spark a lot of debate.
The fact: with a basic JWT authentication scheme, you can’t immediately ban a user. Since JWTs are self-contained and have a predefined expiration, any user in possession of a valid token will retain access until that token naturally expires. You can’t revoke it on the spot, you simply have to wait for it to expire.
At first glance, this might seem like a deal-breaker for JWTs. Ironically, their main strength: decentralized, stateless authentication. Is also the source of this limitation. But with the right design decisions, you can work around this and still build a secure, flexible authentication system.
In the next section, I’ll go over the design patterns I would use to make a JWT-based system more secure and resilient.
How would I implement JTWs securely
First, if you plan on doing something very critical where you need full control of users and being able to ban them whenever you want without risking information security in the slightest. You may just be better of opting out of JWTs and using session-based auth. Or even better, a service that handles authentication for you.
Short life tokens + regen token
This is one of the most common techniques for securing JWT-based authentication. It involves using two different tokens:
- A short-lived access token, typically valid for 10–15 minutes, used to authorize requests to the API.
- A long-lived refresh token, used solely to obtain new access tokens.
Your frontend must include logic to refresh the access token either periodically or in response to authentication errors (e.g., 401 responses). This can be implemented easily using tools like Axios interceptors.
When refreshing the token, the client sends the refresh token, which the server validates before issuing a new access token. This is also an opportunity to check if the user has been banned or should be logged out, in that case, the server simply refuses to refresh the token.
The access token cannot be used to obtain new tokens. And the refresh token cannot be used to access the API.
This separation adds an extra layer of security. You can't immediately revoke access (since the access token remains valid until it expires), but in the worst case, a banned user retains access only for the short-lived token's lifespan. The shorter that lifespan, the more secure your system becomes, at the cost of needing to refresh tokens more frequently. Always store both tokens securely and especially the refresh token using an HttpOnly, SameSite=Strict cookie to prevent access via JavaScript and protect against XSS.
Periodic user checks against DB
Alongside the previous strategy, I would also implement user validation against the database for sensitive or critical actions. This includes things like creating or updating resources, or accessing sensitive data.
If the user's database record indicates that they are banned or their session should be terminated, the backend should block the action. Ideally, if the request comes from an unmodified version of the frontend, the application will also clear the authentication cookies and force a logout.
However, this cannot be guaranteed, a malicious actor could interact directly with the API using a custom client, bypassing frontend logic entirely.
Additional authentication for destructive actions
This method builds upon the previous one by introducing a mechanism to request additional user verification before executing destructive or highly sensitive actions.
For example, to delete a resource, I would prompt the user to re-enter their password (or provide a second factor if available).
This technique is widely used across many platforms. A clear example is GitHub: when you delete a repository, GitHub asks for either password confirmation or a 2FA code.
This approach adds an extra layer of security that benefits not only JWT-based systems but also traditional session-based authentication mechanisms.
Takeaways
I hope that by now my perspective is clear, and one that many others in the developer community also share. JWTs are neither secure nor insecure by default; their security depends entirely on how and where they are implemented.
While user authentication may not be the most natural or ideal use case for JWTs, it is still a valid and viable approach (if done correctly). A fair question to ask is:
Why go through all this complexity when session-based authentication provides stronger defaults with less effort?
That’s a decision you must make based on your specific context. Can you justify the overhead in exchange for reduced database traffic from stateless token validation? Do you actually need a decentralized authentication mechanism?
Don’t pick technologies because they’re trendy. Choose them based on your actual requirements, your system architecture, and a clear understanding of the trade-offs involved.