Necessary qualifier: for browser-based user sessions.
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
> Necessary qualifier: for browser-based user sessions.
> 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)
> 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!
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.
As someone who operates a PostgreSQL database containing 27 billion SSL certificates, each 1-2kb each, with a bunch of secondary indexes that get inserted in random order, I find it pretty incredible that people see the need to optimize their session database. At what scale does the size of the session database actually matter?
Those stateless tokens may be "unforgeable", but they are replayable, and if you're not mindful of that you can have security vulnerabilities.
I think one meaningful case is when you have services in very different locations and you would rather than having to make a request to a session store in a single location, replicate the data to each location for better latency, so in this case a revocation list.
We have a single harbor instance at work, backed by PG. The folks ran into some sort of performance issue during their testing (not documented) and their solution was to use cloud native pg, run two postgres, and add additional second layer of connection pooling.
We eventually hit a bug in a combination of certain images, docker desktop (not even supported by us), and permissions. The error looked related to the connections, so I suggested the pooler be bypassed as a test. They ignored the suggestion for two weeks until it was the last thing to test.
Sure enough bypassing the pooler helped stabilize the connection from harbor during image uploads.
Now I need to talk to them about properly vertical scaling PG before we try and run two with replication, in the same kube cluster, in the same physical data center.
You should do some basic optimizations. Fixed length table and indexes on the unique string for fast lookups. I also like to do a rolling delete for old sessions after 30 days unless mobile session that is logged in. Those get to live forever.
Fair enough, but those optimizations are basically free. People think stateless tokens are free but they really are not.
The cost of the stateless token is basically the CPU usage for signing the message and checking the signature with the public key on the client. Example: Google Compute Instance asks metadata server for OIDC token (which is a JWT). The metadata server respond with the token that basically says "here's the machine service account, here's the machines ID, this token is proof that I am service account abc123 and it's valid for 20 seconds". This is one of the most common uses of JWTs in enterprise. You don't store them. They actually are free.
Lots of web devs get tricked into using them as primary session tokens and it's a huge anti pattern. I see it all the time and people get aggressive about it.
The cost is the vigilance required to use them safely. It's not just compute/storage costs.
I didn't downvote you. You're absolutely right. Implementation of anything is work.
> Fair enough, but those optimizations are basically free. People think stateless tokens are free but they really are not.
Strawman.
The only requirement for a JWT is posting the JSON Web Key set with the public keys used to verify the JWTs signature. That's the full cost of a no-frills JWT implementation of you exclude IAM.
If you want to have one-time JWTs you need to maintain a revocation list. This is literally a set of IDs. If you go nuts and use GUIs for JWT IDs that means each entry takes as much space as 4 ints, and all you need is a set membership check on said integer. Even at FANG scale you can handle that scale in a memory cache service such as ValKey running on a COTS desktop.
Now show us your alternative.
Yes we have heard this before, React is only 30kb! But that misses the enormous amount of infra you need to even just do a basic fetch. (Read the post by the React Query author on whether you need React Query or not)
Likewise with JWTs for sessions you need to handle cache invalidation, revocation lists, key rotation, the list of difficult comp sci problems really does go on!
The same issue as always plaguing the frontend world. Up front “simplicity”, enormous actual complexity
> Yes we have heard this before, React is only 30kb!
Not quite. You might be surprised to know, but the whole JOSE standard, and JWT in particular, specify a very limited set of fields. Whenever anyone starts requiring more than that, the responsibilities start to be offloaded to the likes of OpenID Connect.
This is actually really funny because I recently had this problem having to authenticate an internal repo with an OIDC but the script had to run so early in the bootstrap processes that the python google sdk is not yet installed so I had to manually install the SDK before apt is available by pulling it down manually to bootstrap the chicken or the egg problem. My initial implementation was using curl but folks insisted (rightly) on using the official SDK. I'm sure it's a lot more than 30kb though not during runtime per say.
> If you want to have one-time JWTs you need to maintain a revocation list.
No, you always need a revocation list if you want to handle user sessions in a secure manner. What claims do your tokens contain? If it's anything other than some stable identifiers, like user name, email, permissions, etc. then you now have a cache invalidation problem.
But if all your token carries is an identifier which you need to look up, how's this any better than a signed cookie containing the session ID? All you've done is add complexity.
What do you do about availability? AFAIK the choice is to pick one of 3 or 4 hacky difficult-to-administer clustering solutions, or have that single PostgreSQL database be a SPOF for your whole system.
27 billion? Do you work at AWS?
The issue isn't size, it's load.
I am still waiting for Macaroons to be used widely. I think they are a fantastic invention.
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!
We have what I believe to be one of the world's largest deployments of Macaroons. They're a mixed bag, though I think they're a lot more interesting in a world where agents do most of the fiddly work.
https://fly.io/blog/operationalizing-macaroons/
I am very aware of your work!
It's the only prod usage of Macaroons I know of, I think.
Third-party discharge seems like a great way to have human-in-the-loop gating, among other interesting things.
Would be great reading your thoughts if you ever write about the agentic use case, having all the fly.io experience
I like the raspberry ones. Or lemon is also good
Are you thinking of macarons? Macaroons are coconut.
JWTs can do that (delegate) and such capability is already well defined.
Maybe I stated it wrong. Macaroons have the ability to attenuate the restrictions _without_ contacting the auth server, which makes it IMO fit for restricting and attenuating as much as you want, without much cost.
If I need a roundtrip to the auth server to attenuate, I am not necessarily going to do it as often.
Most token formats delegate. Macaroons support attenuation, confinement, and embedded third-party claims, none of which are JWT capabilities.
Another point is that managing session data on the server-side is a pain... If your app server goes down, stale session data would be left behind in your session store; it can easily become orphaned... So you need to set an expiry on it to ensure that it will be cleaned up no matter what... But you need to keep extending the expiry while the user is still online. God forbid you create the session data before you set the expiry on it and the operation that sets the expiry fails (e.g. server crashes at the precise moment or some error occurs which causes it to be skipped)... In practice, it's hard to avoid stale/orphaned session data.
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.
> 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.
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.
You would think so, but even an authentication company screwed it up:
https://cybercx.co.nz/blog/json-web-token-validation-bypass-...
Clearly trying to be too general. I wrote a tiny JWT validator before that only allows a very small subset of algorithms because I wasn't expecting the JWTs it would handle to have anything else, and obviously not "none".
Wow lol
The people who are upset about JWTs probably got burned by trying to use them in a weird way. Some people try to store sensitive data inside JWTs... WTF, the idea would never have entered my mind! Sensitive data should stay on your server. Encrypted or not! JWTs are supposed to be signed, not encrypted! You shouldn't even think to put sensitive info in there. Also, WTF is wrong with people who accepted algorithm "none." Most of these people who tried to tweak the defaults had no idea what they were doing in the first place.
> Also, WTF is wrong with people who accepted algorithm "none."
They dared to use the default validation function of their JWT library. They did not choose to accept "none".
And the library authors implemented it because it's in the spec. It doesn't excuse that the default was to accept "none", but it is an explanation and in my opinion a valid critique of the standard.
You could have a SQL library that defaults to inserting "OR 1=1" to every update query, which the SQL standard allows. I would blame the library.
The SQL library is meant to run the query you give it.
Yeah well it's definitely a footgun built into the spec but ultimately an library implementation mistake.
Most requests are reads and letting someone use an invalidated session for reads for 30 seconds on a shortlived token isn't the end of the world, especially considering that the exact invalidation timing and its propagation is already somewhat arbitrary.
For rarer privileged actions you can check a token revocation list.
I think both you and GP are somewhat misrepresenting the OP is saying. OP's argument is three-fold:
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.
The design I've landed over the years is to use both. The cookie is a session token and that's where you handle refresh tokens. Then there's an endpoint where you can mint a short-lived tenant-sepecific JWT. This holds the scopes & tenant id. The session token only lets you access the web assets & mint JWT tokens.
A revocation list defeats the purpose of JWTs. If you find yourself needing one, JWTs were probably the wrong choice to begin with.
Not really. Being able to verify a user's signed identity immediately without having to do any database or memory store lookup first is valuable in itself.
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.
Can't a system be DDoS'ed with wrongly signed JWTs as well?
Is signature checking (much) cheaper than finding an opaque session ID in a database?
Yes but it only impacts your stateless app servers which are easier to scale. Your backend services/stores are protected and not affected by the attack.
> 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.
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.
> Some libraries being buggy never was an argument against using libraries. And do you expect your single-purpose code not to be?
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
Come on, it’s not like the two are even within the same magnitude or three
“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!”
If you don't understand conceptually how to verify a signature with a public key the very first thing you should do is get that working and then work from there. It's completely unacceptable to ship without this.
Stateless JWT revocation: https://blog.nellcorp.com/new-aproach-to-jwt-revocation/
WTF:
> 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?
A JWT is usually signed, with a secret you keep in your app. The statelessness of JWT is that it contains all the information you need to verify it. You do not need to ask a db if the token is there and valid.
Storing a user's secret, the same way you store your applications secret does not make it more or less stateless.
In since you now have 2 layers of protection, you don't actually need to verify agains a user's secret immediately, you simply need to check that the token is valid using the app secret. The subset of valid tokens that you need to check is much smaller than the universe of all the unexpired tokens your application has issued.
If you have a security incident and need to revoke tokens for only a subset of your users, now you don't need to rotate your app secret and invalidate every single token and break every single session. You can simply log those users out.
Is author's brain stateless -- my bad, I thought this was not reddit
> [...] you don't actually need to verify agains a user's secret immediately, you simply need to check that the token is valid using the app secret. The subset of valid tokens that you need to check is much smaller than the universe of all the unexpired tokens your application has issued.
What you are describing here is different than what is described in the blog post that you linked to.
Please look at the definition of the function 'validateToken'. In particular, notice how 'getUser' function (which the author notes issues a DB query) is called for every JWT with a valid signature!
EDIT: I failed to realize that you are the author of the blog post. Still my point stands, in that your description doesn't match what the code does.
Maybe I should add a comment there, but this is a compressed example. Everything from getUser onwards does not need to be in the validateToken, it can be done downstream, closer to where you access user data. It does not need to be a separate db call, You pull a user and want to perform an action, so data is in memory, use the secret from there.
Or if you are doing inter service communication, you can use your app secret to validate that the token can actually cross your infra boundary (no user query here), and each internal service can then validate it in their scope, or if a passthrough (like a proxy), just forward it like an envelope.
What this does is give you 2 secure layers, therefore saving you from a lot of the compute (drop expired and globally invalid tokens at the boundary), kill db round trips meant only for token validation (attach to an existing user query you already do) and kill revocation list management.
> Common workarounds like maintaining a blacklist of revoked tokens introduce statefulness, negating the benefits of JWTs.
> Validation: On each request, we validate the JWT's signature using the application secret and then validate the sjti using the user's secret.
Having to lookup the user secret from the db is no different than consulting a list of revoked tokens. You claim consulting a list of revoked tokens to be stateful. How is looking up the user secret different?
"which we'd likely do anyway for authorization checks". Assuming you are using a session token, authentication != authorization. You will eventually hit your authorization logic to check if the user can perform action X. It may just be through claims in the token, but I don't think most are doing that. So you will eventually pull user data and you can to this check there if you are passing the token through context.
I have been using this for a while, and I haven't managed a revocation list, I haven't done user queries at the boundary and users are able to logout and instantly invalidate their JWT. I honestly haven't seen this elsewhere without overhead.
If I need a database query to validate the token, it's not stateless.
Being able to quickly reject invalid sessions identifiers is a useful property in some cases and is normally done by authenticating stateful session tokens with a MAC using a global app key. This can be used for DoS protection if the cost of a database lookup is more than the cost of MAC, and the complexity is justified. It ensures that the random numbers a user is trying to present as their session token are the numbers generated by the server if the key is not leaked.
Because you're trying to bolt things on top of JWT, you're creating a worse version of that stateful authentication pattern:
1. You lost the statelessness of JWT by making database queries. Your claim that "you don't need to verify against user's secret immediately" is false, as you need to do that in all cases immediately after verifying the JWT signature to get the benefits of your system (token invalidation). Sure, you reject completely invalid tokens early, but you still need the statefulness to authenticate users properly (if your goal is to be able to invalidate tokens).
2. In your version, getting a read-only access to the user database (leaking per-user secrets) completely destroys token invalidation, and all your authentication now depends on one key. If, in addition to that, the JWT signing key leaks, user authentication is completely destroyed and can be bypassed by the attacker, who now can sign in as any user. (A common way to leak all this is by failing to properly secure backups).
Compared to a stateful session system with split-tokens, where the database stores tokenId => verifier, where verifier is Hash(randomToken), and user's token is id||randomToken, read-only access to the database doesn't let the attacker authenticate as any user. If the tokens that users presents are in the form of id||randomToken||HMAC(serverKey, id||randomToken) for early rejection as above, leaking serverKey still won't allow the attacker to authenticate as any user. The attacker needs write access.
> Is author's brain stateless -- my bad, I thought this was not reddit
I didn't realize that you were the author, I thought you were a reader who was misled by this blog post. Even better: you can go and edit it, removing "stateless" everywhere! It's fun to invent various protocols, but when someone points out the errors, surely you'd want to fix them -- no shame in making mistakes if you correct them.
Usually, when I think of a protocol, after writing down "Benefits" (as in your blog post), I write "Drawbacks" and then try to come up with downsides and compare it with existing protocols. I'd suggest you do the same.
PS. Find yourself in this picture: http://cryto.net/%7Ejoepie91/blog/2016/06/19/stop-using-jwt-...
1. You're probably correct that some statelessness is lost or I'm creating ambiguity. My concept of it is that you don't need to update the token and you don't need to maintain state elsewhere (ie. a list). Rotating the secret simply means you're forgetting how to validate it, you aren't tracking the state of it in your infra with a revocation list. If we go by the script definition, storing an app level secret, so you can validate your issued tokens is also not stateless.
But if we go with that, and downgrade it to somewhat stateless, I think it maintains it's value proposition well, since I have not seen many JWT applications be fully stateless, especially wrt authorization. So I took advantage of that and bolted this so you can use in during your existing authz flow. So pushing to later does not mean let's pull user data later (that does not change the cost). I mean do it when you have the data so you don't have to pull it just for this.
2. Secrets are encrypted at rest, you decrypt them in memory. Of course, if your app encryption key leaks someone can decrypt all those secrets, but this is an attack surface that already exists. Not trying to address that. Also, each user has their own secret, so you have one app level signing key, and N user signing keys, you need to leak all those and then get the app encryption key to decrypt all of them.
"Compared to a stateful session system with split-tokens" - sure, let me just tell every company that is using JWTs to migrate immediately to stateful session tokens. One sec...done, tomorrow will be a better day for all of us :-)
In all seriousness, I posted this a while ago asking for feedback, HN seemed more interested in AI than actual meaningful conversations. I appreciate the feedback and will update the description and make the examples more clear. The intention there was to compress as much as possible and letting readers implement their use of it where it matched their existing setups.
Regarding your link, I took a quick look (saw it a few years ago, forgot about it), but seems I inadvertently built option 6 "Rotate a user scoped signing key lazily, during authz, on pre validated tokens". This is fundamentally different has a new security and usability posture. And it's still not a session token as it keeps all JWT properties that people are using it for (I make no judgement of whether it's the best solution for them, only that they're using it).
Thanks for the feedback, there is room to improve there.
> First, we need to add a token_secret column to our users table:
> 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.
You don't actually have to do a db trip to get a user secret and revoke a token. A token comes in, and you can store the secret in the same place you store your application secret. Because you do need to store it, cache it, whatever. The point here is you no longer need to keep a revocation database of every token you issued that is still unexpired. Just rotate the signing secret and every token issued until then will be revoked. Goes from maintaining millions of tokens to maintaining a smaller cache of user secrets that are probably rarely updated.
Why not an epoch? because this gives control to the user. They can now logout regardless of token ttl. The point is not obsessing over crypto, JWTs are a cryptographic solution, it's what makes them stateless and I have nothing agains cookies or any other session token. I use them interchangeably.
My pain point was that whenever I needed to use a JWT or whenever I worked a company that used JWTs, their main frustration was "oh but then we can't revoke them easily without maintaining a revocation list". Well now they don't have to.
Telling them just migrate to "this or that technology" is not how this works.
What? So instead of storing a revocation list, you store a per user secret that you need to consult. What is the difference? How is that stateless? How does it avoid a “rb round trip” (where are you storing the user secret)?
Wouldn’t it be simpler to use a session token? This complex machinery does nothing but look fancy.
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?
"We only need to consult the database for the user's secret..." , which kinda defeats the purpose.
JWT used to be bad due to libraries with poor defaults. Downgrade attacks were fairly common a number of years ago.
Since most of the common libraries across all languages have gotten more sane defaults, it actually is pretty secure nowadays.
If we stipulate that, we're still left wondering what the utility is of a standard that creates affordances for the insecure defaults, as opposed to just designing it right from the beginning.
> utility is of a standard that creates affordances for the insecure defaults
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.
No, that doesn't square up. It's like arguing "you could say the same thing about TCP, because it allows you to build JWTs, which are a bad protocol".
Not the same. HttpOnly/Secure cookies were added much later and was not the default. They should have been inaccessible to JS by default from the beginning, and this policy has been a source of countless attacks.
If memory serves me right, cookies were designed by Netscape in 1994 before JavaScript was even a thing. They were released in an early beta of Netscape (0.9 something), while Javascript was only added in Netscape 2.0. SSL 2.0 was only added in Netscape 1.0. So the HttpOnly/Secure attributes were not relevant to the first cookie design, which wasn't even a standard.
When the first cookie standard (RFC 2109) was released, the Secure attribute was added. You could argue they missed HttpOnly, but JavaScript itself was highly non-standard and underspecified mess during this period (and for a while later too). Almost nobody was thinking about XSS as far as I can tell, and that term was probably only coined at least 2 or 3 years later (by Microsoft researchers[1]). At 1997, the people who even considered XSS, probably only saw this as an HTML injection issue that can be fixed at the injection site and doesn't require any special protections against JavaScript code.
If you really want to point a finger at issues in the early cookie design, then you could talk about domain matches. Not making the port part of the matched domain and not allowing an explicit way to trigger an exact domain match with a specified domain was a mistake. And the confusing leading dot rules (probably for optimizing a substring match without parsing the domain components) was also a mistake.
But RFC 2109 was replaced with RFC 6265 and now RFC 6265bis (which is not released yet, but is mostly implemented by the big browsers). These RFCs fix most of the big issues we had with cookies, and do not shy away from breaking existing behavior: setting SameSite=Lax as the default and restricting SameSite=None to secure contexts broke A LOT of sites for improve security. The changes made in RFC 6265 to forbid multiple cookies in one header also broke many sites.
An equivalent approach in the JWT spec would be a new RFC that does the following:
1. Forbids implementing Unsecured tokens and removes alg="none".
2. Removes RSA and ECDSA (or at least deprecate them) and make Ed25519 the "Recommended+" signature algorithm. If we allow RSA and ECDSA for compatibility, they must be explicitly enabled with feature flags or some other marker that signals their insecurity and security advisories on their potential vulnerabilities must be attached.
3. Removes the entirity of JWE as it is currently implemented (to be replaced with encryption else that is NOT orthogonal to authentication).
3. Requires that HMAC secrets are specified as binary base64url data. They SHOULD be generated by a CSPRNG or a derived using a safe derivation method (such as HKDF-SHA256 with safe key material) and MUST be at least as long as the "security size" for the algorithm (i.e. 32 bytes for HS256, 48 bytes for HS384 and 64 bytes for HS512).
4. Makes the "exp" claim mandatory to set, and mandatory to verify.
5. Add a section with strict implementation guidelines for libraries, e.g. `parse()` functions that skip verifying the token should have a clear name like `inspect_without_verification()` or `dangerously_parse_without_verifying()` and `verify()` function should always receive a key that is strictly typed to a specific algorithm.
6. Remove or restrict the usage of fields that allow the JWT sender to dictate the keys used for verifying it, like "jku" or "x5c". For instance, the standard can mandate that when implementing these fields, the verifier MUST NOT accept any JWKS URLs that do not match one of the explicitly allowed patterns or X.509 certificates that are not signed by an explicitly trusted CA.
[1] https://www.youtube.com/watch?v=mKAWpFdVcPY
Spec writers and library authors are human? Who knew
I don't understand what this is meant to communicate. The standard is either good or it isn't. "Good effort" is not an engineering assessment.
Your objection is that they should be "designing it right from the beginning" but that applies to all realms of endeavour. The reason they didn't is human frailty.
If everyone simply designed everything right from the beginning we would live in nirvana.
I read an article about business which had this classification, "Would be weird if it worked", "Might work", and "Would be weird if it didn't work" and argued that you want to be in the last category.
In engineering we aspire to a slightly stronger standard: "I made it physically impossible to fuck this up."
And yet https://en.wikipedia.org/wiki/Hyatt_Regency_walkway_collapse...
Yeah I don't think they were aspiring to that
You've completely missed my point. I don't even accept the premise of the JWT standard. But the eventual migration to safer default settings, in a format that continues to expend implementation effort to support settings nobody should use, is in fact a practical engineering problem with the standard.
And they've published updates[0] and libraries have hardened their defaults and removed support for insecure values (e.g. alg='none'). I'm not sure what more you want?
I'd rather use a refined, battle-tested standard with lots of eyes on it than some new untested contender produced by a handful of upstarts ("look, we just designed it right from the beginning! This time it's perfect!") PASETO reeks of second-system syndrome.
[0] https://www.rfc-editor.org/info/rfc8725/
I don't recommend PASETO either.
What do you recommend then? What technology has been designed, completed, then used for years without any updates or problems?
Bearer tokens are a dead end? You have to validate them anyway so traditional auth is the fallback.
https://fly.io/blog/api-tokens-a-tedious-survey/
tl;dr: most of the time you should use opaque random strings.
API tokens are a very small narrow part of the authorization universe. Having a shared secret relies on a trust relationship between the resource server and the identity provider that does not exist between, say, my SaaS backend and Google or Meta's login system.
The OP was talking about sessions (which include session cookies and API tokens). I'd argue these use cases are far more common for the average programmer than tokens and signatures that are used for federation, but I'll bite the bullet here:
JWT is a serviceable solution for service trust and federation. This use case often just requires a very-short-term token, so lack of revocation support is not an issue. Replay attacks are still an issue, but they can also be prevented with single-use nonces that are included in the token claims.
The OP's take (and my take as well) is that JWT is rarely the BEST solution for this use case. You kinda have to use it if you need to implement a standard that mandates JWT such as OpenID Connect. But OpenID Connect is a great example for a place where JWT was used, but was never really necessary. If you do use the authorization code flow securely (on the server side, with a strong client secret and proper CSRF protection) you don't need the ID token. In fact, you don't need to use any cryptography at all! Just like random session IDs, you've got a stateful solution that works reliably without any cryptography.
If you cannot do a series of authenticated network requests between HTTPS endpoints to verify trust, then a signed payload could be useful, but you've got better standards than JWS/JWT for that. That's all.
> If you do use the authorization code flow securely (on the server side, with a strong client secret and proper CSRF protection)
This restriction precludes all desktop clients, mobile clients, and webapp clients -- any place where you can't trust the client code to protect a secret.
I don't exactly disagree with you: Security becomes much easier once you rule out handling all the hard edge cases.
PKCE, OAuth 2.0 for Native Apps and the Device Code flow are a thing. In practice all of these clients work so well with OAuth 2.0, that the implicit and resource owner password credential grants have been removed from OAuth 2.1 and are the latest OAuth 2.0 BCP forbids the password grant and strongly recommends against the implicit grant.
... so, then, there is a need for something other than a shared opaque random string API key?
I feel like I'm being argued in a circle by a series of strawmen.
https://www.latacora.com/blog/2018/06/12/inter-service-authe...
PASETO and TLS 1.3 were also written by humans. TLS libraries (which are several orders of magnitude more complicated than JWT libraries) are also written by humans.
If you passionately care about security and misuse-resistance you CAN write a spec that will lead to fewer implementation issues.
You must be young if you're pointing to TLS libraries as an example of doing it right and not getting into trouble with insecure implementations and downgrade attacks. https://wiki.freebsd.org/LibreSSL
I wish I was young. Did I explicitly said TLS __1.3__ or did I not?
A lot of effort was put into making TLS 1.3 a stronger, less agile and more misuse-resistant standard than its previous iterations. And that effort worked.
Not for long
JWT libraries had poor defaults because the spec was poorly designed.
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
Lots of very bad uses for JWTs for service-to-service communication, too. There are often way more standard/foolproof alternatives than how lots of people use JWTs on the backend.
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.
Wow, Fortune 500 companies are using an insecure technology, get hacked and exploited by cryptominers and PII burglars and then just patch their vulnerabilities and call it a day? This never happened before! /sarcasm
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.
Yeah, hasn't it been "best practice" for a decade or more to treat JWT like a ticket and swap it for a cookie-based session ID in anything browser-like? Then you just do all the cookie session "best practices" to lock it down.
I agree with your first part but your edit is a logic fallacy. I don't need to be able to hack something to say that it is insecure.
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.
> Please, if JWTs are such a horrifically insecure standard, go ahead and publish your means for hacking AWS STS's AssumeRoleWithWebIdentity
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.
JOSE can still have problems if it's secure when implemented properly. A lot of API surfaces for them can kinda suck. If secure when held right was equivalent to good, then that would apply also to stuff like X.509
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.
> Necessary qualifier: for browser-based user sessions.
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.
There is in fact a long lineage of vulnerabilities caused by JWTs in real applications.
[dead]
Why is JWT so messy? base64(something) + "." + base64(something else)?
Why not just base64(JSON.stringify(everything)) ?