Plenty of good uses for JWTs for service-to-service communication.
edit: I read some of the linked stuff, e.g. https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba... . Please, if JWTs are such a horrifically insecure standard, go ahead and publish your means for hacking AWS STS's AssumeRoleWithWebIdentity , or don't publish and just exploit it by launching cryptominers in every Fortune 500 production AWS account. Let me know when you inevitably succeed, because JWTs are so insecure, right? /sarcasm
> Plenty of good uses for JWTs for service-to-service communication.
This is the sensible conclusion right there. I agree JWTs are the wrong tool for the use case of user sessions in the browser.
To give some more arguments:
All the signature and encryption stuff in JWTs is complex. While common JWT libraries have now mostly got their stuff together, this has not always been the case. There were plenty of libraries accepting the "none" algorithm [1] or allowing attackers to forge tokens by using a public key as a shared secret [2]. This is the direct result of the complexity criticized in the linked blog post.
JWTs also cannot do some stuff you want for user sessions. You can't invalidate them without keeping a revocation list somewhere. But if you have to check an identifier for revocation on every request you could just use an opaque session ID and look that up on every request instead! Sure, you can use short-lived tokens and refresh them all the time, but why bother with that for a typical application that has to keep some state anyway?
All that being said, I wholeheartedly agree that there are use cases in distributed systems and machine-to-machine communication where signed tokens can be useful. Just please don't confuse the two cases.
[1] https://nvd.nist.gov/vuln/detail/cve-2022-23540
[2] https://nvd.nist.gov/vuln/detail/CVE-2024-54150 (just a random example from googling, I don't know what library made this one infamous)
One reason could be the size. A revocation list only needs to keep session IDs of recently logged-out sessions, for which the token's TTL hasn't yet expired. It may be a much smaller list than a list of every active session.
Also, a JWT (or a Macaroon, etc) can store a large amount of details about the session in a cryptographically secure, unforgeable way. This rids you of the necessity to store all that in your active session database, again cutting the size.
Those stateless tokens may be "unforgeable", but they are replayable, and if you're not mindful of that you can have security vulnerabilities.
It seems they were not of very much use in the past, but with the agentic-everything now, I see this as a great way of delegating permissions to subagents, third-party agents, etc.
Working on something along these lines but unfortunately I cannot dedicate as much time as I'd like.
Still, if anyone is reading, give Macaroons a try!
And yes, you need to store and manage more data and your session store is an additional Single Point of Failure... With JWT the revocation list is an optional... Your system can keep running without it; it just won't be able to ban users. It's a cleaner separation of concerns without SPoF.
JWTs have so many benefits over session IDs, I could write a book about all the benefits. Sure, there are some tradeoffs but the negatives are typically pretty minor or hand-wavy.
This is a red herring. Applied cryptography was never considered an easy subject in software engineering circles. Neither was algorithms and data structures. Yet, it's still a basic tool, and developers are still expected to understand things such as why some maps allow reads in constant time while others require log time.
Some libraries being buggy never was an argument against using libraries. And do you expect your single-purpose code not to be?
I think we are seeing in this thread a knee-jerk reaction against perceived complexity. Yet, if you sit down and compare JWTs with alternatives and list all features along with pros and cons, you'd be hard-pressed to even try to put together a case for JWTs being bad.
Of course you should use battle-tested and well-maintained libraries for the really hard stuff such as cryptography primitives. However, that is not the point I was trying to make.
My points here are:
1) If you can get away with not using cryptography for something, you probably should. Your web framework already supports session cookies. Even if it doesn't, it's very hard to mess up opaque tokens from SecureRandom or /dev/urandom and a corresponding database lookup.
2) If you actually need the things JWTs can do, the standard is still needlessly complex and easy to mess up in ways that are not inherent to the problems JWTs are trying to solve. I'm not saying this means you should roll your own solution (again, I agree that there is value in well-tested libraries), I'm further strengthening point 1) with this. Don't use JWTs if you don't need to
I'm a bit surprised at this. These are extremely simple to solve - the first time I ever did a JWT-reading implementation I specified the right defaults, which are very simple, even for a mid-level backend person I would say, and they haven't needed changing in 8 years or whatever it's been. It really isn't very complex.
https://cybercx.co.nz/blog/json-web-token-validation-bypass-...
1. JWTs are not a good fit for a session token (although there are several RFCs that are trying to shoe-horn JWTs into this use).
> TLDR: JWTs should not be used for keeping your user logged in. They are not designed for this purpose, they are not secure, and there is a much better tool which is designed for it: regular cookie sessions.
2. JWTs have other "valid" use cases that only need a very short-lived token (e.g. a transit token or a request signature) and don't need to care about user authentication, revocation, XSS etc.
3. But JWT should not be used even for the "valid" use cases, since you have better (read: less outrageously insecure) alternatives nowadays.
> Also note that "valid" usecases for JWTs at the end of the video can also be easily handled by other, better, and more secure tools. Specifically, PASETO
You've noted these issues yourself. There are many common vulnerabilities with JWT: alg=none, algorithm confusion and weak key brute-forcing, mandating weaker algorithms like RSA and ECDSA while making the best, fastest and easiest to implement algorithms like EdDSA "optional".
There are also other design deficiencies that JWT makes by trying to be a generic cryptographic envelope format rather than a token format: e.g. expiration can be omitted and this feature that caused some libraries to not verify expiration by default or have a different (and confusing) set of token parsing methods that do not enforce the expiry. PASETO is a better design that is secure against all of these issues. Sure, there are a few minor qualms I can find with PASETO (e.g. no mandatory key ID and no support for non-JSON payloads), but it's unlikely to face the same avalanche of CVEs we got with JWT libraries.
For rarer privileged actions you can check a token revocation list.
With session IDs, anyone can spam/DDoS your system much more easily; they don't even need to be authenticated to waste your computing resources as they can send plausible-looking session IDs and your system will waste a ton of resources querying your session store or database to figure out that the session doesn't exist... It adds a ton of latency and wastes CPU cycles across multiple systems. Also stateful systems like Redis are harder and more expensive to scale than stateless systems like application servers. Not to mention that they may be depended upon by other parts of the application so hitting those too hard can be more disruptive. And that's kind of best-case. Some people use a database to store their session data...
At least with a revocation list with JWT. If the JWT says that the user is user1234, then you know that this is a real, previously logged-in user, they have an account at stake, you can afford to spend a bit of time/resources to check them against your revocation list... And if they are on that list, you can ban their ass and they're done! They'd have to create a new account of they try to spam you again. They can't spam without a valid JWT and they can only get that by authenticating with your server first.
Sure with JWT, some computation is spent on verifying the signature but it's very cheap using the default algorithm and only touches one system... And you only need to call another system once you know that the user is valid.
JWT is much more robust for DDoS prevention because of this. But yeah, you don't necessarily need a revocation list. You can use short JWT expiries with frequent token refreshes. Revocation list is good if you need immediate ban.
“But if you have to check an identifier for revocation on every request you could just use an opaque session ID and look that up on every request instead!”
> Each user has a secret: Stored securely in the database.
> Stateless Validation: The core validation remains stateless. We only need to consult the database for the user's secret, which we'd likely do anyway for authorization checks.
Is "stateless" the same as "serverless" now? Is author's brain stateless?
> ALTER TABLE users ADD COLUMN token_secret;
So it's "stateless" but we have to query the users database on every request? How is that more stateless than SELECT * FROM session WHERE id = cookie?
Ignoring that and taking the mechanism as given: Why the obsession with cryptography, in this case HMAC? I don't see any reason why another signature is needed here when I believe the same outcome could be accomplished with a token_epoch field in both the signed JWT and the users table. Just increment the epoch to revome old tokens. Or even better, drop the epoch field and have an iat_not_before field per user. The field in the JWT is signed, the whole point is that you can trust it.
Do let me know if I miss anything here please. Assuming I haven't: it's always puzzling to me to see people being so eager to sprinkle more cryptography on anything that is supposed to be secure. For me, I've become more afraid of cryptography the more I learned about it. Cryptography is hard. It's not a magic ingredient for security. At best, it's dangerous black magic -- very potent, but pronounce a single syllable of your magic spell wrong and it _will_ blow up in your face.
The application secret is redundant if the per-user secret is used.
Also I’m inferring from the article that the author is using symmetric keys (HS256) for their JWTs. In what world can you securely distribute symmetric keys but can’t use an opaque session token?
Since most of the common libraries across all languages have gotten more sane defaults, it actually is pretty secure nowadays.
You could make the same argument about Cookies.
> as opposed to just designing it right from the beginning
And generally, it's quite difficult to design it right from the beginning because one would often start with the wrong assumptions. Most standards evolve, and it should be acceptable.
Of course JWT can be implemented securely. Even XMLDSig can be implemented securely. But if the spec is not designed with security and misuse-resistance as a tier 1 priority, you will get more issues. The fact that we didn't see the same sheer volume of issues with PASETO or macaroon libraries (admittedly, the later are far less numerous). I can find only one CVE for a PASETO library from 2020, and this is an issue that has nothing to do with the algorithm itself (JPaseto < 0.3.0 switched the order of two arguments in their hash function call, generating weaker hashes).
The reason PASETO won't have the same issues as JWT is the design (especially with v3/v4). There is no alg=none, symmetric keys are fixed size (so no weak keys can be used) and algorithm confusion is prevented by an explicit implementation guide[1] that strongly mandates that keys for different algorithm version have different types, and verification functions MUST reject a key of the wrong type.
Is JWT safe now? Maybe. A lot of issues have been fixed, but new issues keep coming all the time. We're not even halfway into this year and I can count at least the following serious 2026 CVEs: CVE-2026-28802, CVE-2026-29000, CVE-2026-1529, CVE-2026-22817/8, CVE-2026-34950, CVE-2026-23993, CVE-2026-32597, just to name a few. Most of them are the same classic alg=none, signature verification bypass and algorithm confusion issues.
The issues is that new libraries are coming all the time and the vulnerability elimination process for existing libraries is just a random scattershot. If a security researcher has happened across a vulnerability in library X and reported it, it's solved. If nobody has found it yet: though luck. Unless you pick a library that has been officially audited for these issues, you don't really know if it's truly safe. If you use a PASETO library, it's probably not audited either, but the chance of it having these common types of issues (and other issues, like psychic signatures[2]) are close to nil.
---
[1] https://github.com/paseto-standard/paseto-spec/blob/master/d...
[2] https://www.securecodewarrior.com/article/psychic-signatures
True. Still the article can't really make any case, other than pointing misuses and throwing a few baseless assertions.
In fact, it surprised me to see such an article featuring so many upvotes.
The primary use for JWTs is to allow resource owners to perform stateless JWT validation,and then be able to trust a JWT's payload to perform authentication and authorization. This doesn't mean what the blogger think it means. These processes become stateless because the resource owner does not need to perform any inline request to be able to tell whether the JWT can be accepted or not. Meaning:
- The JWT is signed and/or encrypted, and the resource owner can use it's JSON Web Key set verify and/or decrypt it.
- the JWT, once deemed valid, includes metadata that helps the resource server determine if the JWT should still be accepted. This includes timestamps of when it was issued at and when it expires, a JWT ID to check a revocation list, and even a few user claims such as user id, audiences, scope, etc. A resource owner does not need to perform any request inline to perform those checks. The revocation list needs to be kept fresh but it can be refreshed as a background task. At most, if the JWT is expected to be single-use, the resource owner is able to run the nonce/JWT ID through a denylist.
- one of the primary values of JWTs is performance. For the vast majority of usecases, the whole verification&validation flow is stateless. This means no outbound request is needed to execute authentication and authorization checks. Instead of plowing through something between 20-100ms of latency to handle auth in each request, the whole flow takes less than 1ms.
I don't think the blogger fully grasps this nuance. Outright asserting that JWTs introduce performance issues completely erodes any trust that the blogger has a solid grasp on the subject.
I feel like discussions like these usually will surface PASETO/Macaroons/Thin Mints as the fix without acknowledging that the complexity of distributed token passing with arbitrary attenuation isn't a fit for most use cases.
That all being said, sometimes JWT is the right solution for the job! It's the core skill of software engineering to be able to take in all the arguments and tradeoffs and make the right choice for your scenario.
Full disclaimer: Take what I say with a grain of salt because I build a project (SpiceDB) that advocates for a more centralized approach for one of the backend JWT use cases: fine-grained authorization.
Just because a certain practice is popular, doesn't mean it's good for security, and it definitely does not mean the companies who do this never get hacked. Popular != Unhackable. I don't believe this needs to be stated.
Cases in point:
- Passwords limited to 8 characters
- Passwords hashed with a fast, single-iterated hash (with or without salt, that's not the main point, we are not in 2003 anymore goddamnit, and GPUs are a thing!)
- Passwords stored in cleartext
- Using old-style C/C++ without bounds checking and fuzzing and treating stack overflow exploits as just a fact of life we'd have to live with, while most other languages don't get anymore (and if you have to use C/C++ for reasons there are ways to prevent this).
- Injecting unverified user input directly into SQL strings.
- Using ancient software without ever patching or updating vulnerable versions.
The standard and AWS' specific implementation thereof are two different things. Can you afford a security org the size of Google or Amazon's security orgs? If not, you are playing a different ballgame.
For example: I don't know how to exploit SAML but I know it is a terrible standard dur to making all of the XML parser an attack surface. I am not a security researcher so I dont know how to find exploits in XML parsers but I know having a huge attack surface is bad.
There are better alternatives for a lot of cases, standard session tokens or API keys are a popular one in use in most major websites online and work pretty much perfectly for most use cases.
I'm not gonna say those standards are completely without merit. The best thing about them is that it is some basic standard on passing stuff around that isn't like ASN.1 encoded or whatever, to which the tooling seems incredibly brittle and bug-prone.
CVE-2026-28802, CVE-2026-29000, CVE-2026-1529, CVE-2026-22817/8, CVE-2026-34950, CVE-2026-23993, CVE-2026-32597.
Most of them are the same classic alg=none, signature verification bypass and algorithm confusion issues.
Why not just base64(JSON.stringify(everything)) ?
When using sessions, your list of valid sessions is probably orders of magnitudes higher that the revocation list - thus the data lookup costs and the storage cost of that statefulness is higher.
Plus, the article mentions JWTs are stateless but that is usually not true. You mostly not only validate the JWT, but also obtain a matching identity object (i.e. user details) for each request to see if the user is still enabled/authorized to do whatever he does. You can leverage stuff such as per-user revocation lists, or a minimum_issued_at that will validate any JWT iat field. This allows the "Logout from all devices" pattern, where that action will simply set a user's minimum_issued_at field to $NOW. All previous tokens will thus be revoked, without individuall revocation list checks.
There are systems where the authorization is done in the JWT too (i.e. scopes/permissions in the token) - in that case you are right.
Now the reasonable response to the above is that this should be happening in a dedicated authn/z concern - and that is correct! But when paranoia is called for, it's not unreasonable to have redundant checks in logic where authz is critical.
DRY conventionally refers to repeated sections of code, not redundant communications.
2. Sessions by definition are ephemeral. A database should not be necessary at all, an in-memory cache should suffice.
3. If you really need to distribute session data across multiple nodes, just propagate them asynchronously. Authentication and authorization are semantically idempotent operations. Having to possibly re-auth when making a cross-region request within milliseconds of logging in might be mildly annoying for the user, but consistency isn't a deal breaker here.
With JWTs, you would only need to replicate your revocation list of the last X hours (X being your JWT default lifetime) and probably be in the megabytes for the total list. Easy to replicate that ever 5-10seconds to all your locations.
Sessions have expiration timestamps too, and you can configure them however you like.
Yes, and a lookup operation is a lookup operation.
Your database or data structure used for storing the sessions/JWT revocation entries won't really care whether you look for things that are active or things that are inactive/revoked. If you store it in the right database, both lookups will be O(1), so it is the same (or at least the difference is negligible), regardless of the size.
JWTs are just tokens like session data but in JSON format. What format you choose to go with doesn't matter.
You can keep storing JWTs in local storage and still be secure. Discord removes it on page load and restores it when the tab is closed.
Also if your website is susceptible to XSS, skill issue, exactly like in the case of SQL injections. That wouldn't have happened had people used the right tools and not played with fire.
One of the advantages of JWTs is that you don't have to check your database or filesystem to make sure the the user is valid and logged in. All that data is in the JWT. If it's just a static page, it doesn't need to hit any data.
The problem then comes that some developers think that makes it secure, and don't check the database for revocation before doing anything with the account. Especially not for giving out private data. They might check before changing any data.
I think it's a really neat idea that is far too easy to mishandle and create a bad situation. It can save a lot of bandwidth and CPU cycles if you have a lot of non-interactive pages and all you need to know is whether to show that the user is logged in or not. But for actually doing anything, it's practically no better than a session cookie, and it's got a lot of foot-guns.
sqlite3 cookies.sqlite 'SELECT name, value FROM moz_cookies WHERE isSecure AND isHttpOnly'
And that's a supposedly a master password protected browser. They can't even bother encrypting cookies. Don't be ridiculous.
Local storage isn't any better in this regard
Is it? If an attacker can't do XSS then it's as strong as cookies.
Supply chain attacks aren't an argument here because they can also happen with cookies. CSRF as well. The same can happen in actual executable binaries.
I don't get the 20 yr age argument:
- HttpOnly fights XSS which is impossible to execute with modern frontend frameworks.
- SameSite fights CSRF but the real solution is to disable loading the website in iframes (remember clickjacking?).
- Secure fights MITM which is already fixed by default when using local storage and HSTS is the real deal.
Having said that, I'd say that local storage is more secure than cookies (no need to remember whether you put Secure on or not). Unless you're still using PHP, which means touch grass.
Eh. Frontend frameworks tend to make successful XSS much worse because they tend to require disabling HttpOnly for not very good reasons. HttpOnly is a nice defense in depth measure against the consequences of XSS.
> - SameSite fights CSRF but the real solution is to disable loading the website in iframes (remember clickjacking?).
Disabling iframes doesn't fix CSRF. You can still <form method="..." /> or <img /> tags or whatever. For an example, see these universal logout pages. SameSite helps with CSRF (you really should also using CSRF tokens as the primary control and maybe using the Sec-Fetch-X headers as well).
How does this work? You have no real control over what the browser does when it closes a tab.
Second problem - if they remove it from localstorage they still need to hold them somewhere. So are they moved to a simple variable then? It's just as accessible as localStorage. Maybe it's randomized every load?
What happens if you lose power? You're logged out because it didn't save the token back. Verified this by pausing JS execution and killing Firefox. (the local storage key is "token")
>The JWT specification itself is not trusted by security experts.
This feels like it needs more evidence than just one blog post. And that blog post seems to just largely blame bad implementations? Something that will plague any standard.
Overall, I don't know what I expected clicking a random gist link.
Beyond this, you can make shorter lived JWTs just fine in the browser and have the agents self-update. If you use Azure Entra or a number of other providers it works this way in practice... you keep your JWTs relatively short lived (5-15m) and can even check for jti revokation.
JWTs are incredibly useful for separating/reusing an access authority from your applications/api systems. You shift the attack surface and do it in a way that can be trusted. We use PPK for lots of things, including SSH all over the world. No, I wouldn't use shared secrets and I wouldn't use long lived tokens... but short lived, ppk signed tokens from verified/known sources are generally fine.
For that matter, it's often API keys that are really problematic. Just had to implement them... for me, the API key presents as a Bearer token as well, but there's a short "sak." prefix then an identity part (base64url uuid bytes) followed by a secret as base64url bytes... in the database is the uuid and a passphrase level salt+hash from the secret.. so the api key generated should be treated as a secret and is one-way to the database, so a db breach doesn't breach auth.
Even then, an API key leak is far mroe likely than a problem with a well implemented JWT solution.
On a linked page, there's also this:
> Any JavaScript code on your page can access local storage: it has no data protection whatsoever. This is the big one for security reasons (as well as my number one pet peeve in recent years).
This is a weak argument. You know, just don't put "any javascript code" on your webpage? Limit it to trusted javascript code? If you allow random people putting random javascript on your webpage, you have already lost anyway!
This seems like one of those scenarios where you make different trade offs depending on your threat model. The author's threat model sounds similar to a news site where they track and advertise so they're forced to run semi-trusted js.
100% agree. This is common sense to me and I'm always surprised to re-learn people don't do this
JWTs are too long lived... Nothing is stopping you from limiting the JWT lifetime and having a refresh model against an authentication authority... I mean, even if you use cookie based sessions, you're storing somewhere... you can have a jwt valid for 5-15min. 15minutes is roughly the cache timing for many authorization systems including Entra... and even a 5min token with a refresh system can be used fine from a browser.
Lastly, I prefer to have identity/auth separated from the application/api services... it externalizes context and JWT per request is easier to deal with than some shared cache/state system that may intermittently fail as opposed to a signed token that you can verify the signature against known authorities.
OIDC tokens are all JWTs btw.
Because the ID token is not a set of credentials, but signed information you’d use to create/update a user’s profile.
You can technically use JWTs as access tokens, as the spec doesn’t specify a format for access tokens, but in my experience they’re normally opaque bearer tokens.
It would have been better if instead of implementing ID tokens, OIDC only supported the authorization code flow and returned a JSON payload of claims (which nobody would incorrectly assume to be trustworthy).
> And there are more security problems. Unlike sessions - which can be invalidated by the server whenever it feels like it - individual stateless JWT tokens cannot be invalidated. By design, they will be valid until they expire, no matter what happens. This means that you cannot, for example, invalidate the session of an attacker after detecting a compromise. You also cannot invalidate old sessions when a user changes their password.
> You are essentially powerless, and cannot 'kill' a session without building complex (and stateful!) infrastructure to explicitly detect and reject them, defeating the entire point of using stateless JWT tokens to begin with.
I'm not sure that this is entirely true. Typically, the total number of non-expired issued tokens is much higher than the number of invalidated unexpired tokens. Therefore, if you store only invalidated tokens and delete them when they get expired, you can significantly reduce the amount of required storage and the cost of lookup.
Although, in any real application the performance gains will be minuscule (compared to the cost of, you know, everything else. Auth is just a small part) and probably not worth the extra complexity.
[0] "Stop using JWT for sessions" - http://cryto.net/~joepie91/blog/2016/06/13/stop-using-jwt-fo...
> I'm not sure that this is entirely true.
You can be sure it is not true, because it is utter BS. JWTs have an "iat" timestamp field (issued at) and in the described case that an attacker has a leaked token, your validation logic simply should refuse any token with iat < $NOW for that identity.
I have JWTs implemented for a site and in my case, users cannot individually revoke tokens - but they have a "Signout from all devices" option. That will basically just set a field "minimum_issued_at" to $NOW in the database for their user, and any tokens will always be validated against the minimum iat timestamp. That is a good compromise in security and simplicity.
Revocation lists have their purpose, though, in systems with heightened security requirements.
Well, this approach throws out a lot of babies with the bathwater. You invalidate tons of legitimate tokens along with the one that you wanted to invalidate and get a thundering herd [0] of clients wishing to re-authenticate.
This is probably not good in case of a really high load.
And if you don't have a really high load, then there is no good reason not to have a stateful session storage.
You are not throwing out a lot of babies with the bathwater if you would do it in a case of a known attack. You would invalidate ALL tokens of a user, which is a sane default especially since usually you wouldn't be able to rule out what other tokens were compromised. And yes, if it later turned out ALL your users and all their token were possibly compromised because you had some kind of security flaw, setting a global minimum_issued_at is exactly what you would do after you fixed the flaw. And yes, that means all your users must reauthenticate.
makes no sense
... ok now it does :) your now is not now, but a stored value
Does anyone have an example of how they built a JWT revocation service?
If you want to be more fancy and fast, you can use bloom filters to check if a token is in a revocation list.
This has several advantages, the main one being that sub-services do not have to interact with the authentication database or have access to the capability to mint tokens (this assumes you use RS256 not HMAC). So if a sub-service gets compromised it's not as devastating as a service which has access to the authentication database.
If you have sensitive data inside the token you should use JWEs, although they're not as good because you have to ask an internal service (which has the private key) to decode the token each time you want to use it.
My typical layout is {"id": (uuid), "scopes": ["scope:read/write"]}.
Also they're really neat for SPA's as you can have your static site server validate that the JWE with the public key before serving any resources. The way I use this is that I have my static site compiled to /(scope)/path and the static service will not serve pages that you cannot access anyway. This is very useful in cases where you have administrative panels where you don't want to expose to users what capabilities your backend has or/and expose the internal service paths that can be attacked.
My lifetime for JWT's is around 5 minutes for "backend access", things like /me are cached in localStorage unless explicitely instructed in /refresh to drop localStorage cache. My request handler in my SPA applications detects "refresh required" and refreshes the token.
I think most of the blame here belongs to node/next and python libraries. I write my backends in strongly typed languages and my frontend is always made out of precompiled static pages. My current setup for the frontend is using VITE with prerendered pages for landing and normal SPA for applications.
With all of that said I strongly disagree with this entire gist. JWT is as secure as you want it to be.
So we switched to that main service obtaining the client's JWT itself from the authorization service, and then handling refreshing it on its own. That means that if the client e.g. buys some new feature, they still need to refresh the page (so the new connection to the main service is made) to see it working, but it's always been this way even before, so... eh. We had to scale the auth service a tad, but other than that, it worked fine.
If the re-minting happens transparently with a user interaction then you spread out some of the request velocity that can come with that (if you're operating at a large enough scale for it to matter for this to be a concern).
This point is not made very clearly and is buried by overemphasising JWTs instead of just quickly pointing them out as an example of a stateless session. But yeah, it is a good point.
And what if the user is logged in from multiple devices, but only wants to log out from ONE of them? Your solution logs them out from all of them.
The entire point is that it is not possible to have authentication that is both: 1. stateless. 2. secure.
And so if authN is going to be stateful anyways, you might as well just use an opaque token in a database and eliminate all the complexities and foot-guns of JWTs.
Doesn't address logging out a single session, though
Anyhow, there are way smarter people than myself who have covered this topic extensively over the years, but I still think that, even in 2026, JWTs are the wrong tools for web auth. They're fine to use for service-to-service stuff, but if you have the option, just use PASETO -- it solves a lot of the issues!
You are right that you can do oauth2 without JWTs. But JWTs are a common enough base technology that it is standardized separately under the IETF along with a whole range of related standards. As does the OAuth2 standard and of course a whole range of standards built on top of that and related standards such as OpenID Connect.
The base specification does not mention JWTs, mainly because it predates the JWT RFC by a few years. That's something that's being rectified in the v2.1 draft of the spec which recommends the following:
> It is RECOMMENDED to use asymmetric (public-key based) methods for client authentication such as mTLS [RFC8705] or using signed JWTs ("Private Key JWT") in accordance with [RFC7521], [RFC7523], and their update [I-D.ietf-oauth-rfc7523bis] (defined in [OpenID.Connect] as the client authentication method private_key_jwt).
So, JWT usage is indeed not required, but very strongly related if you look at the broader ecosystem of specifications and implementations. It's hard to ignore JWTs in that context.
A user wants to access a read-only resource with an invalid JWT? Envoy bounces it without passing the request through to the backend. Valid JWT? Let the request through without having to look up any session information. No DB, no cache, no session server hit. Fast.
A user wants to change a password, email address, or add an authenticator? First, require a password, second, require a second factor. If all of that checks out, look for the JWT access token in a revocation list that is only accessed during sensitive, infrequent, requests like these. If the token has been revoked, 403.
Tokens are dropped from the revocation list once the original access token's TTL has passed. Which should be low. I use 5 minutes. Most sessions on my site last 4-10 minutes.
Worst case scenario, a malicious user is able to access certain read-only resources for a few minutes.
I've clearly spent too much time working with data covered by HIPAA because this sentence gave me a brief bit of panic. The vagueness and extent of what it technically covers means it's far safer to just assume literally everything about your users needs maximum security.
JWT is a signed JSON blob.
Cookie is a storage and transport specification.
Local storage is a storage spec.
A "regular" cookie could also be a signed cookie which is basically the same thing as JWT.
Slight disagree in horizontal scalability--server sessions scale somewhat with Redis, replicated DB but obviously not to the degree stateless ones do.
Also on revocability, you don't need to revoke the token if you're validating fine grain permissions outside the token. You can revoke the permissions (ie disable the user). You can use JWT to gate permissions at a high level (infrastructure, traffic edge, API gateway) then validate fine grained permissions in code
Session cookies you exchange an opaque value with the DB for the user info
JWTs the user hands you their driver's license, and you can verify that it's an authentic license for the person who's name is on it
In application terminology, a session is user state that outlives a single request.
Depending on what definition you use and how pedantic you are, a stateless signed cookie is also a session cookie.
Why JWT is bad: it's a cargo cult solving a non-existent issue in a more complicated way than necessary. An HTTPOnly session cookie containing just a random ID is shorter and easier to handle.
Why JWT is also bad: a typical way to use it exposes too much attack surface. Almost every JWT library has way too much functionality, supports multiple algorithms, and many people are too sloppy with their dependencies, so you probably haven't read every line of code that runs in your auth.
How to use JWT safely:
1. Have a use case that cannot be easier solved with just a random session identifier. For example, one party creates tokens and another unrelated party verifies them. If same party issues and validates tokens, you better have a super high load, unique use case -- but then you're senior enough to not take random advice from strangers.
2. Write your own JWT handling code. It's literally a few lines of code to create tokens and a few dozen to validate. Only implement the exact algorithms and claims you use.
3. In a typical scenario, JWT should still carry something like a user ID which you should immediately verify against a database. Stateless sessions doesn't mean no DB lookups on validation. If you DO authenticate based on the token alone, the token should be super short lived (seconds or single digit minutes).
For web/mobile auth where same server issues the JWT and same server verifies it, it makes no sense. JWTs cannot be invalidated. If a user loses some permission or account gets disabled, JWT will still be valid until its expiration time. Servers must either make DB calls to verify the user is still active or be fine with deactivated users having access for a while after account is disabled. This completely defeats the purpose and bearer tokens work perfectly for this use case.
JWTs should _almost_ never be used in client side auth. Client should send regular cookies and bearer tokens. The auth server can internally generate a short lived JWT and inject it into requests before they get routed to various services internally so those services don't need to query DB every time to verify the user.
I'm not being facetious, genuinely asking - is this a big deal? Should be a pretty cheap query, and with pooled connections, hardly any overhead.
If your backend is a set of 12 services where each needs to verify the authenticity of the request then it might start adding up. In that case using JWT _after_ the initial API gateway makes a lot of sense. The gateway hits the DB, authenticates the request, mints a JWT and injects it into downstream requests. Then each service on the request path just verifies the JWT.
Overall I don't think hitting the DB for auth is a big deal and that is why we don't need all the bells and whistles JWT brings. Just use bearer auth.
I don't see another setup that comes close to the ease of setting this up - add an endpoint that provides jwt tokens to valid sessions, done. With user-individual permissions.
Just to clarify, httpOnly/sameSite isn't useless under XSS the way localStorage is. XSS can't read a httpOnly cookie, so it can't exfiltrate the credential, it can only perform the attack during the session from the victim's browser. A JWT in localStorage can be reused offline for its entire lifetime. Also worth separating: localStorage is the exposure, not JWT. Just please for the love of all that's good and pretty, don't store a JWT in a httpOnly cookie.
Depends on who is saying, I've read the same thing but the other way around. Never store a JWT in LocalStorage and always store it in a httpOnly cookie.
It's far better than a session ID in the sense that you can store an accountId inside a JWT and be confident about the identity of this user... The fact that you can get such confidence without any external lookups; just by looking at the token, is incredibly useful on its own. You can already seperate out unauthenticated guest users from authenticated users and you can tie their identities to real accounts without even checking the DB or session store. Banning is a separate concern. Being able to quickly, efficiently and reliably identify a user from the server-side is a necessity.
The only real downside of JWTs which people point to is that they cannot be easily be revoked... But if you really need the ability to revoke quickly, you can easily have 10 minute expiries on your tokens and request refresh every 3 minutes or so... So if you want to ban someone, there would be a 10 minute delay which is acceptable for the vast majority of scenarios... You should handle rate limiting as a separate concern anyway if spam is the issue.
For certain systems, a user may be making hundreds or thousands of requests in 10 minutes so you're saving a HUGE number of session ID lookups and it means that you don't need to run and maintain a separate Redis service or whatever else. Not to mention the additional latency which is added when you need to check Reddit before each operation.
These blanket statements against JWT are not new. People have been misusing them and blaming the tech because they don't want to acknowledge that they made implementation mistakes.
Any technology can be misused. It's foolish to misuse a perfectly good piece of tech and then use that as the basis to promote some alternative which has been through less battle-testing and which probably has even more gotchas which are yet to be discovered.
As a senior engineer with 15+ years of experience, I've seen this cycle over and over again. The new tech is always presented as fool proof and it never never never is.
I just noticed that after JWT was created, people would just slap on JWT like an end-all because JWT sounded secure and they thought it was all that they needed to do.
That’s my only “problem” with JWT but to be honest, people will build insecure systems anyway.
Using them as the primary source of truth is an anti-pattern like the blog post is actually saying.
> they are not secure.
They are secure if they fit your risk profile, a blanket statement like this is just disinformation.
Don't treat your peers like idiots.
I think the irony is that the people who make these blanket statements may be idiots themselves and that's why they think everyone else is an idiot. They can't imagine that other people don't have problems using those tools. It's a skill issue.
Some people here built embarrassingly parallel distributed systems with consistent hashing load balancers. JWT is easy by comparison.
To me, it sounds like a child putting their shoes on the wrong feet, getting blisters and concluding that nobody should wear shoes.
> The JWT specification is specifically designed only for very short-live tokens (~5 minute or less). Sessions need to have longer lifespans than that.
Your auth token should be 5 or 10 minutes long, yes. Your session should be encoded in a refresh token, which can be another JWT or completely opaque, whichever you prefer.
> "stateless" authentication simply is not feasible in a secure way. You must have some state to handle tokens securely, and if you must have a data store, it's better to just store all the data. Most of this article and the followup it links to describes the specific issues:
No. Stateless auth is completely feasible in a secure fashion. You need short lived tokens as stated above, and to perform instant logout you need to be able to identify JWTs that were invalidated at the level of your API gateway. You don't need to store every JWT that is valid, only a way to identify the ones you want to invalidate early. If you make bulk invalidations not go onto this store (and just accept that with bulk logout, users will remain logged in for up to 5 minutes, which if you're doing logouts in bulk, this does not matter to begin with), then this typically fits in memory. "if you must have a data store, it's better to just store all the data" is just wrong.
And of course for refreshing the token, you go to your data store and recompute things. But the point is to do this every 5-10 minutes like the author (correctly) identified, not on every API request.
> JWTs which just store a simple session token are inefficient and less flexible than a regular session cookie, and don't gain you any advantage.
But no one does this. People use JWTs as auth tokens which are short lived but contain a bunch of information about the users that you'd get in huge trouble if it could be forged (user ids, resolved geolocation, entitlements, cohorts, etc etc), but that you also don't want to make every service of yours look up against a data store or compute for every single API request. It should be data that normally doesn't change during the duration of the auth token. When it does change, you should have a mechanism to immediately invalidate the previous one (see above) and issue a new one on the API call that made the change. Simple.
> The JWT specification itself is not trusted by security experts. This should preclude all usage of them for anything related to security and authentication. The original spec specifically made it possible to create fake tokens, and is likely to contain other mistakes. This article delves deeper into the problems with the JWT (family) specification.
Yes, all standards are horrible if you look at them long enough. XML is terrible, YAML is terrible, ASN.1 is horrific. I'll take a flawed standard that has been studied and criticized publicly so I have libraries and know where the footguns are vs rolling everything myself and having to find out the footguns on my own.
The only case where JWTs are actually useful is when the producer and consumer do not share a DB (eg OAuth/OIDC, iot deployments, heavy microservice architectures) otherwise a good cached session store should handle well up to a few orders or magnitude below google scale.
Some nice topics to talk about instead:
- When to use an encrypted value (and symmetric or asymmetric), vs. a random (but secret) value, vs. a signed value (readable but not tamperable)
- Where to put these values (memory, localStorage, cookies)
- How to make sure these values don't last forever, and whether you need to be able to revoke them (make them invalid before their natural expiration timestamp)
The idea that they are related to sessions is, unfortunately, a very common misconception. I wrote an article about this some time ago: https://dev.to/andychiare/sessions-tokens-and-rocknroll-4pdc
https://datatracker.ietf.org/doc/html/rfc9449#name-dpop-proo...
The JWT specification itself is not trusted by security experts. This should preclude all usage of them for anything related to security and authentication.
Very bold claim that seems to ignore all the iteration and hard-won lessons on this from the ecoystem...This year, I ended up publishing an open protocol that can be used for everything from secure authentication to API requests to micropayments, and it works with the existing Web stack and P256 curve with JSON serialization curves as well as EVM blockchains via K256 curve and EIP712 serialization.
I encourage everyone to take a look at it and consider using it, so we can stop reinventing the wheel. Everything has already been deployed, it’s an extremely simple protocol, much simpler than JWT and requires no global registry.
https://github.com/OpenClaiming
It is also used in stuff like https://safebots.github.io/Safecloud/
This article just sounds like heebie-jeebies, at best it's someone saying something about JWT doesn't smell right (because it can be used incorrectly,) at worst it's a pissing match / religious war.
This article would be more credible if it had a tangible explanation of a real exploit.
(BTW: I don't understand how cookies are inherently "better" or "worse." They can be sniffed and replayed too.)
https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba...
It boils down to "there were bugs in some of the libraries" and then goes on to recommend you...pull in libsodium and do it yourself??? This is ludicrous advice that I simply can't take seriously. All software has bugs. The whole Internet lost its shit with Heartbleed, but we still use TLS and OpenSSL.
> The JWT specification is specifically designed only for very short-live tokens (~5 minute or less).
I've never heard this before and can't find any evidence to back this claim up. RFC 7519 doesn't make any such claim.
> Sessions need to have longer lifespans than that.
Why is this JWT issue. JWTs generally have refresh token, which are database validated and we need to just refresh once for 100s of calls in 5 minute.
> "stateless"
Yeah this sounds bad and I don't think people do it.
> JWTs which just store a simple session token are inefficient and less flexible than a regular session cookie, and don't gain you any advantage.
Ok, but it is not worse too.
> The JWT specification itself is not trusted by security experts. This should preclude all usage of them for anything related to security and authentication. The original spec specifically made it possible to create fake tokens, and is likely to contain other mistakes. This article delves deeper into the problems with the JWT (family) specification.
What are they even trying to say?
[0] https://paragonie.com/blog/2017/03/jwt-json-web-tokens-is-ba...
Yeah, right.
JWTs are fine, as long as you use sane algorithms. The missing revocation really can be done by replicating revocation policies. E.g. a direct list of revoked tokens or a blanket "don't trust tokens before this timestamp".
But yes, short life times with frequent renewals is necessary; that's obvious though. And same applies to any other auth tech.
- Use ES256
- Always set the JTI to be completely random
- Set iat (issued at time) and exp (expiration time)
- Set iss (issuer) and aud (audience) to match your application
- Set sub (subject) to match whatever unique identifier you use for users
Store the hash of the JWT in your database with a lazy cleanup hook on the expiration time.
Now, you can use this JWT for a cheap WAF at the edge!
Token expired already? No need to query the database, reject.
Audience doesn't match the requested URL? No need to query the database, reject.
Signature doesn't match your public key? No need to query the database, reject.
Everything passes? Query the database for the token hash.
Token hash not in the database? Add the token hash to the WAF's cache (with lazy cleanup hook on the expiration time).
Everything passes but token hash in the WAF cache of rejects? No need to query the database, reject.
etc
See the benefit? It's defense in depth. If you screw this up, all you lose is the WAF layer.
I don't think the cryto.net post really explains why this is true (at least in a way that would be made different by "massive resources").
and the JWT can transport a more encrypted user session as a value
just be intentional
> If you do need a short-lived, signed token for something, there is a better spec called PASETO which is designed to be secure
Suggesting to non security people (like myself) something for auth that isn't a mainstream idea seems like a bad idea? Not to mention that it doesn't refer any reasons why it's better
This is quite a loaded statement. Why is it better to store all the data? What if you have a CDN layer that only needs to do routing based on authentication or scope, or other token encoded data?
Citation needed. Where does it say this?
The JWT specification is specifically designed only for very short-live tokens (~5 minute or less). Sessions need to have longer lifespans than that.
Huh? The expiry is as long or short as you want.
The JWT specification itself is not trusted by security experts.
...and we're supposed to trust some random gist? Pure appeal to authority.
Please, keep using JWTs, they do their job well: giving you an access or ID token that you can pass between applications and trust based on cryptographic signatures from an identity provider.
> A lot of people mistakenly try to compare "cookies vs. JWT". This comparison makes no sense at all, and it's comparing apples to oranges - cookies are a storage mechanism, whereas JWT tokens are cryptographically signed tokens.
And yet the author seems not to have noticed, or something? Odd.
If you are operating at a scale where you can simply store session data in the database and look it up every time, that's a fine way to operate. At some scale this approach becomes a problem, and it's faster/cheaper/simpler to store some limited data on the client (signed).
Yes there are complexities to both approaches. That's fine.
The post is not descriptive enough
It should explain how to not store JWT instead of just saying JWT is bad.