What's with the shouty title? Well, I wanted to grab your attention and get straight to the point:
🗣️🗣️ Don't use JWT for your backend authorization
Look, there's a time and place for every piece of technology and the tricky part is determining if your use case actually is the time and place. Hopefully this post will walk you through why JWTs might not be your best friend, and the rare cases where they actually make sense.
🔄 Quick Crash Course: What's a JWT?
So, JWT (pronounced "jot") stands for JSON Web Token. It's part of this whole family of specs called JOSE (no way!) that deal with encrypting and signing JSON. JWT is the cool kid of the family - it's defined in RFC7519 and gets all the attention. Why? Because while its siblings (JWA, JWE, JWK, JWS) handle the nitty-gritty encryption stuff, JWT is the one carrying the actual payload.
Think of a JWT as a JSON object wearing a fancy coat (some headers) and carrying an ID card (a signature) to prove it's legit. It's got these things called "claims" - like when it expires (exp
), who created it (iss
), who it's for (aud
), and so on. The most popular claim for authorization is called "scope", which, fun fact, isn't even from JOSE - it's borrowed from OAuth2. Most developers end up mixing and matching these pieces like a authorization puzzle until something works.
⚔️ The New Enemy Problem: JWT's Achilles' Heel
Here's the thing: JWTs have a major weakness - once they're out there, you can't take them back (except waiting for them to expire). It's like giving someone an all-access pass and not being able to revoke it if they go rogue. This becomes super awkward with web sessions - ever tried implementing a proper "logout" with JWTs? Good luck with that! You're basically crossing your fingers hoping users will play nice and throw away their old tokens.
But wait, it gets worse for backend services. Imagine this: you revoke someone's access on your server, but they're still holding a valid JWT from before. They can keep accessing stuff they shouldn't - this is what the smart folks call the "New Enemy Problem" (first spotted in Google's Zanzibar paper). It's like changing the locks but forgetting about all the spare keys you handed out. Centralized authorization systems fix this by having a central service (think of it as like a bouncer at a bar) checking everyone's credentials in real-time. The New Enemy problem is a really hard and interesting distributed systems problem (and perhaps a future post here)
An example of the New Enemy problem:
- Alice removes Bob from the ACL of a document;
- Alice then asks Charlie to add new contents to the document;
- Bob should not be able to see the new contents, but may do so if the ACL check is evaluated with a stale ACL from before Bob's removal
📏 JWT Scopes: Not as Fine-Grained as You'd Think
While JWTs look good on paper, things get messy in practice. Remember that scope claim I mentioned? It's... kinda vague. The spec basically just says "here's what characters you can use" and calls it a day. You'll see examples such as 'email profile phone address
' floating around, and developers often try to get fancy with stuff like 'profile:admin
'. But here's the million-dollar question: what does that actually mean? The whole site? Just one user's profile? Even GitHub's REST API has been wrestling with this for ages!
Modern apps need super specific permissions - we're talking granular stuff like 'issue/authzed/spicedb/52:author
' instead of just 'issue:author
'. When your users might need access to billions of things, you can't stuff all that into a token that's bouncing between services.
Centralized authorization is like having a smart assistant who keeps track of everything in one place. Need to check something? Just ask! For example: SpiceDB does this using something called ReBAC (Relationship-Based Access Control) - it's like a Swiss Army knife that can handle super detailed permissions while still playing nice with other permissions systems such as Role Based Access Control (RBAC), Attribute Based Access Control (ABAC), and other fancy patterns. Google also uses ReBAC for authorization across their services such as YouTube, Docs, and more.
🔮 The Crystal Ball Problem with JWT Authorization
Let's play pretend and say you're cool with using just a few JWT scopes. Even then, you've got a problem: how do you know what permissions you'll need? When your JWT gets created at the front door (like in an API gateway), it needs to predict what every downstream service might want. For anything beyond a super simple setup, that's like trying to predict next week's lottery numbers!
Plus, if you send a token with too many permissions to the next service, you're basically giving attackers a bigger target to hit. This headache led to the creation of Macaroons. These tokens can actually be trimmed down before being passed along - cool idea, right? But in reality, they're so complicated that most folks who tried them ended up saying "thanks, but no thanks."
Centralized authorization systems take a different approach. They're like "Hey, we know we can't predict the future, so just ask us when you need something!" Sure, you have to make an extra call, but systems like SpiceDB are optimized to keep data in-memory - so latency looks similar to reaching out to any other cache like redis or memcache.
🤔 So... Are JWTs Ever the Right Choice?
After all this JWT-bashing, you might think they're completely useless. But there is one scenario where they shine: one-time grants where access cannot be revoked. Though honestly, that's about as rare as finding a unicorn in your backyard!
What system do you use for your authorization needs? Let me know in the comments below.
UPDATE 1:
There's a nice discussion in the comments about either adding state to the JWT, or a system of using a denylist for token revocation. Both these approaches have their downsides and can be fraught with errors. Check the comments below for more info
Top comments (11)
You can implement sessions and include them in the JWT, and during the JWT validation (probably in a middleware) you can check if the session is valid, this way you can invalidate a session and even if the JWT is valid you block access.
You can also use a blacklist, you can implement it with redis and add JWT tokens within this blacklist and consult this blacklist before authorizing in the middleware.
While that approach has been used in the past @wiliamvj , I wouldn't recommend it and here's why:
In the first proposal the JWT holds an identifier to a session which is managed server side like this:
Then if the session ID is valid server side, you move forward. This functions like a cookie. This way you lose the trust, independent verification, and statelessness in the JWT because of the addition of a state to it.
In the second proposal, there's addition of a token revocation mechanism, which has it's own set of downsides. One of which is that it relies on reading a negative list of what's NOT allowed, which can be attacked more easily (e.g. DDOS) than reading from a list of what IS affirmatively allowed (e.g. inserting a signing key).
The thing is Token Revocation doesn't really solve the New Enemy problem unless on every authorization mutation you can individually track down and revoke every impacted token, or rotate your signing key and issue all new tokens. Both proposals are fraught with errors.
Similarly for the sessions, you would have to track down which sessions are impacted by each and every authZ update (and their transitive downstream implications) and update or invalidate the sessions.
Hope that clears it up!
I use JWT for authorization. But if i stop using JWT, what other best alternatives do i have?
Well that depends on what your usecase is @keyru_nasirusman . Here are some possibilities:
Writing your own custom authZ solution is an option and there are libraries (such as CanCanCan, Flask Principal etc) that handle this for you. I'd like to add that while it's a good learning curve to write your own custom authorization, doing so in complex production environments can get messy very fast.
If you want to abstract your authorization logic from your code you can consider a policy engine. The policy engine is centralized (maybe running in a sidecar) and all authZ requests go out to this engine. This works great when you already have all of the data ready at the time of the call. For ex: An HTTP filter or something like that where the data that you're making your request based on is already available to you in the form of the IP address of the caller or so on.
If you are building for scale and want a system that is flexible, you can look into Relationship-Based Access Control. This is a fairly new system and is gaining in popularity. You essentially model all your relationships and store them as edges in a directed graph. The advantage is that it can really scale. For example: this is the system Google uses across all their services such as Cloud, & Docs with a service they named Zanzibar. There are some open source software that are based on Zanzibar as well.
Feel free to ask more questions, or tell me your usecase and i'd be happy to assist :)
Thanks, I will read more on the options you mentioned above (I am interested on the Zanzibar based softwares). I will also be glad if you write an article on any of alternatives of JWT.
I will definitely be writing about Zanzibar soon. Cheers!
Maybe add a clarification for those who might confuse Authorization and Authentication.
That's a fair point as people often conflate the two
Authentication is about making sure the user is who they say they are. You're proving your identity with something you have like a username and password.
Authorization is the next step. Once a user's identity is confirmed, authorization decides what parts of the application they are allowed to use.
So, authentication checks who you are, and authorization determines what you can do or access in the application.
Give a read to OAuth2 specs, refresh tokens, and short-lived access tokens - you might change the way you look at this.
OAuth scopes face the same problems as pre-canned roles - Sometimes they're not fine grained enough, and if you just do one-scope-per-thing that can be done, your scope list gets too big to store and manage effectively.
Here's a real-world example: You have a scope for an Admin role. Now suppose you need a billing-admin (only change billing settings), or auditor (see everything, do nothing), or an assistant (federate access, do nothing).. you get my drift.
Eventually user-defined roles really break the OAuth paradigm.