1. shorter lifetimes with no other revocation system. This works well if you go back to a central party which has the actual business logic/state to decide whether to issue a new token
2. token introspection API, possibly with caching to reduce network calls/user latency
3. API-based blacklist with invalidated token identifiers (either JTI or SID)
In the first two approaches you are trying to prevent state from being pushed out to the apps at the edges.
The third approach is what I took with Distributed Token Validity API, which was basically a distributed system (via state replication or fetch + cache) to move the minimal state needed as close to the app as feasible.
https://stackoverflow.com/questions/14682153/lifetime-of-ker...
Ed: Note that the service doing the renewal won't need the user/password - only a way to check if the user is blacklisted, as well as a way to sign/make tokens - and doesn't have to be the same system that issues the initial token.
1. The shorter the lifetime, the more JWTs and refreshes are needed. This includes the signing and verification, both of which can be heavy operations depending on key length.
2. Token introspection is a coupling hub and spoke. If every service needs to introspect every JWT, it will be coupled to the introspect API. While that isn't horrible in some cases, it can be a nightmare in others. Additionally, new services need to always remember to use this pattern. You can get around this with an API gateway though. In either case, the introspection API either needs to store all the JWTs (in memory or a database) to determine if they have been revoked, or use some other mechanism to determine revocation. If you have large numbers of JWTs valid at any time, this doesn't scale well.
3. This is basically the same as my approach. It sounds like you are using a distributed cache. I'm using Webhooks and multiple local caches. Basically the same thing if your Webhooks are written properly.
But agreed, webooks in general are not excellent for critical events in general unless there are additional ways and of confirming receipt and confirmation.
Here are some things to consider though (I work on this problem a lotttt):
1. Do you (as the app developer) really care about security that much? If not, just go with local validation on your JWTs and understand that there is a window of time, whatever your token lifetime is, that you will be at risk for compromise and abuse. If you do care, continue down the list, if not, just don't worry about it.
2. So you care about security, yey! Now, your problem is this: what level of risk are you OK with? If you're ok with NO risk, continue reading. Otherwise: set your JWT expiration time to the amount of risk you're ok with. EG: 1 minute, 5 minute, 1 hour, 1 day, etc.
3. OK! So you're REALLY serious about security and want no risk. Now we're talking! You really only have one choice: validate your JWTs centrally to ensure that a user hasn't been deleted, token revoked, etc. But now you have to decide the best way to do this! Here are your options:
A) You can validate tokens centrally on every request with the IdP: this is basically the same thing as normal old session management: you check the session against a DB to validate it.
B) You create a cache of tokens that are blacklisted (aka: a blacklist), so that you can do an O(1) lookup to see if a token is expired. The only problem? This is usually centralized, too! So no real benefit over option (A) except that it's a bit quicker (if you don't make mistakes w/ cache invalidation, etc.).
C) You do what this article talks about: you distribute your blacklist directly to your edges: one way to do this is with webhooks, websockets, or really just about any method of syncing data between places. This is a great approach, but unfortunately, a lot more can go wrong: what if your webhook doesn't get processed quickly enough? What if your client breaks and stops being able to accept webhooks? What if the webhook server stops firing or gets overloaded?
In my personal opinion, (A) is the best choice, because it's the simplest, the hardest to get wrong, and can always be cached to speed things up. The only problem with (A)? The only problem is that if you're doing it this way, why use a JWT at all? Why not just use a normal old cryptographically signed session ID? Not much benefit, IMO.
> what if your webhook doesn't get processed quickly enough?
I'm assuming you mean at the edges. This is possible, but if you are on the same backplane, highly unlikely since our solution is simply inserting an int into a Hash. Even if it isn't backplane, then you still can retry. Think of session caches as the same situation, so really no difference if you have a distributed cache for (A). Also, FusionAuth provides configurable timeouts and transactions for Webhooks. This covers most failure cases.
> What if your client breaks and stops being able to accept webhooks?
I'm assuming you mean "service" rather than "client" since we want the services to know a JWT has been revoked. I'd say this should hopefully mean that the service is also not accepting API requests. If it is, then you will have issues. However, this is a failure case no matter what. Similarly, the client breaks and stops validating the JWT in (A), you are in the same situation. Plus, FusionAuth provides transactional Webhooks, so you can retry and protect against failures.
> What if the webhook server stops firing or gets overloaded?
Since FusionAuth is the Webhook event generator and the IdP, if it stops, you got bigger issues. :)
In terms of overloading, we protect against event backlogs and failures pretty well. We can also shut the entire node down if needed to prevent any API calls. This is a bit brute force, but if you are in #3 for security, turning everything off is better than a breach.
Beyond the trivial JWT implementations, trying to figure out how to persist JWTs (stay logged in) and revoke access immediately got way more complicated than it needed to be when a lot frameworks already have sessions implemented and secured properly for a good while now.
You didn't cover the case (2.5) where you make a best effort to invalidate immediately, but don't need absolute guarantee of that as long as you know it will be eventually consistent, where 'eventually' is bounded. This relaxation of requirements can be liberating.
Didn't read TFA, but another way to distribute the blacklist to the edge is to return it along with other data. This isn't going to be a portable library or subsystem, if it works at all it depends very much on your arch and it will be custom.
> why use a JWT at all
because the JWT can carry other information, eg authz, in a portable way and that can be consumed by other backends. the portability of the JWT may also allow for much easier debugging. why not use a JWT? there's no disadvantage if you just process it like a normal signed session ID.
1. You are now passing large blobs of data over the network more frequently than just a signed session ID 2. If you want to ensure consistency of your data (for security purposes), that extra data embedded in the claims of the JWT are useless since you need to query for them for freshness after validating the session anyway.
Once you persist it - you might as well be using a session. I suppose it depends on your scale - maybe with just a couple of thousand users it would work ok to store every JWT issued for their lifespan.
I agree that persisting the JWT means you may as well be using a session, but my takeaway from that is that you should just use a session, not come up with more weird workarounds to justify JWTs.
FusionAuth seems like an amazing CIAM for anyone to use. I will definitely check it out.
Is the core open source? I wonder what language it’s written in?
One of the advantages of JWT, especially with public/private key signing is that you can verify a token without a potentially long network/service request or lookup adding latency to every API request. And if one API passes down to internal APIs it's even more of an impact.
In the end, pick your poison, but if you want to expire them, may as well generate an identifier as a token/cookie and lookup against Redis, or another session database/table for the identifier itself. Having the data in the payload doesn't do much good if you're validating against another service for every request anyway. If you want broader replication Cassandra or a similar database with nodes for replication near each server could help mitigate things, again no need to include the JWT payload in the token at that point though.
Using hooks to propagate revocations is almost worse, because it's fragile and may see misses in practice.