Hmh, the way I usually use JWTs is as an authentication cache. You obtain your authentication token from the auth service which grants you permission to other services.

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.

At my last work we used to use "the client obtains the authorization token from the auth service, and supplies it to other services" too, but at some point we found that a) permissions in the JWT grew a tad too large to reliably pass them in the HTTP headers; b) localStorage is finicky, as is reliably refreshing it — Safari on MacOS apparently turns off the JS timers if the user looks away from the open page; c) the client can actually see how we represent permissions internally; d) the client only really ever faces a single most popular service anyhow; e) that most popular service used only JWT for authentication as well, so stealing a JWT token was a problem.

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.

irrevocable* cache

I've adapted dynamic public-key hotswapping whenever there is a need to revoke tokens as it would simply force all tokens to go to /refresh endpoint instead of the standard 5m cache. Never had to use it though. I've experimented deriving the public key from the uuid so I could broadcast that "keys with this id and this revision should no longer be accepted and should be refreshed", but as I said never ran into a situation where 5 minute expiration wasn't fast enough. That said if you're dealing with critical infrastructure JWEs are the way, you just lose the speed benefits of JWEs as you have to make a request to an internal service to validate and decode, but for everything else JWTs are completely fine.

This is an interesting approach especially if you factor in that re-minting a key is usually a lightweight task compared to what most API calls have to interact with.

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