Hacker News new | ask | show | jobs
by RagingCactus 5 hours ago
> 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)

7 comments

> 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.
The issue isn't size, it's load.
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 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!

JWTs can do that (delegate) and such capability is already well defined.
[delayed]
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.
> 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-...

Wow lol
A revocation list defeats the purpose of JWTs. If you find yourself needing one, JWTs were probably the wrong choice to begin with.
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.
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

> 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.

"We only need to consult the database for the user's secret..." , which kinda defeats the purpose.