1. Overall, no net benefit. We still had to query a k/v store on every action to check that the token wasn't revoked.
2. Less secure because there's no way to make it act like an HTTP-only cookie (no access from JS).
3. No way to revoke all of one user's tokens because there is no list of those tokens. We want to limit (or be aware of) the number of active logins and let the user sign out from all locations (as you can in Gmail). If the token is only stored on the client, there's no way to do this. You could store a list of all granted tokens and only use it for these revocations, but that's yet another level of complexity. (When we dropped JWT, the diff was -1200+95 LOC because we had to handle a lot of scenarios like this.)
4. Adding tokens to client-side requests is a pain. The common example for e.g. Angular is to use an HTTP interceptor to add a header. More code, but it works... Except for img and a (anchor/link) elements where you can't add a header. So you put the token in the URL query instead, and now you're vulnerable to session leaking if a user shares a URL with someone else. And you have to manually update those embedded tokens when they expire, as many JWT folks recommend you make the expiry short.
The expectation should be reduce in database io, not getting rid of it entirely.
Overall you could issue tokens with short lifespan, say 5 minutes. And having the client refreshing tokens periodically. Reading database every 5 min is certainly an improvement over reading it on every API call.
You could also only check revocation table if the performed operation is security-critical or sensitive enough. I wouldn't care if the user is trying to perform a read-only operation on some public data with a revoked token that is going to expire in a few minutes anyway.
>3. No way to revoke all of one user's tokens because there is no list of those tokens
I don't see why one would not maintain such a list, since generally issuing a token requires database io anyway. Storing a token when issued is just one additional write op.
This means that internal microservices don't need to deal with session management at all (including revocation), and only need to worry about validating the public key of the signer.
Yet if anything is accidentally exposed (let's say a microservices fetches a user-provided URL, and the URL is localhost), the user still cannot spoof an identity without having the signer's private key.
The internal JWT also has a very short TTL, so accidental leakage wouldn't cause much trouble.
I would not use JWT for session management.
Second benefit is, at least for me, standardization.
Edit: fixed typo
It's never a bad idea to understand the basic structure of how a framework might be doing the work for you behind the scenes though.
I think the problem is that it's not about issuing just one JWT, you need a 'refresh' token and a 'security' token. The security token expires quickly, but a new security token can be issues using the refresh token which has a long expiry.
You can even make the refresh token single use with the action of refreshing the security token returning you a new refresh token. This, however, needs to be handled carefully within the UI as two requests using the same refresh token would invalidate the 'session'.
The reasoning is that the api can trust the claims within the security token, but has no need to access external sources to validate the token. The refresh token can have security policies applied to it as well as 'user logged out' checks against the refresh token. This keeps the API requests extremely fast with only an intermittent (once a minute) need to get a new security token.
JWT itself, however, is a signed-JSON format with detached signature which is very useful well beyond access token use-case.
JWT as access token at large scale will require frequent key rotation so inability to revoke becomes a problem but it's not a show stopper because one can work around it using various tactics such as bloom filter I mentioned above.