At AuthZed, we believe there’s a time and place for every piece of technology; the tricky part is determining if your use case actually is the time and place. For many years, there’s been a strong argument by domain experts against using JWTs for web sessions. While this campaign has succeeded to help improve the security of the web frontend, there hasn’t been an equivalent campaign for the backend. While building SpiceDB, we’ve surveyed many backend developers only to find that many don’t know the pitfalls of JWTs or even that alternatives exist. SpiceDB is an open source project that implements one such alternative called centralized authorization. Because of this, I’ll be sure to include exactly how a centralized strategy accounts for the pitfalls with JWTs, too!
Refresher: What is a JWT?
JWT (often pronounced “jot”) is an acronym that stands for JSON Web Token.
JWTs are part of a set of specifications for encrypting and signing JSON called JOSE.
Each component has its own specification; JWT is defined in RFC7519.
How come no one ever talks about the other JOSE components (e.g. JWA, JWE, JWK, JWS)?
Because JWT stands out from the other components—it’s the payload format that can be optionally protected with encryption strategies specified by the other JOSE components.
At the end of the day, JWT is an encoded JSON object with some extra metadata (headers and a signature) to ensure integrity.
The properties on this object are called “claims” and some are standardized for declaring when the token expires (
exp), who issued the token (
iss), who the audience token was (
aud), and more.
The most common claim used for authorization is called
scope and actually doesn’t even come from any JOSE specification; it comes from the specification for OAuth2 Token Exchange.
Backend developers often piece together the most useful combination of these ideas to get something that works for them.
JWTs Don’t Pass the New Enemy Problem Test
JWTs have a fundamental flaw: they can't be revoked when their claims or scopes expire for reasons other than the expiration stored in the JWT. If you’ve read about JWTs usage for web sessions, you might already be familiar with the issue. For web sessions, the inability to revoke a JWT leads to insecure implementations of “logging out” of a web application. Because you cannot revoke existing JWTs that are valid before the “log out”, you’re forced to wait until they expire or hope that the client faithfully discards any old JWTs when they perform a “log out”.
For backend services using JWTs for authorization, the inability to revoke JWTs can become equally, if not more dangerous. Because you cannot revoke a JWT, a user could have their access revoked on the server, but still have an old-but-valid JWT that contains a claim for accessing content they no longer should be able to access. The ability to use a stale authorization token to access newly restricted data is called the New Enemy Problem. This scenario was first described in Google’s Zanzibar paper and is a litmus test for the security of authorization schemes. Centralized authorization strategies solve this problem by having a central service that is contacted to perform permission checks. This service is used to provide a consistent view of permission changes to enforce consistency and protect against the New Enemy Problem. This is a really hard and interesting distributed systems problem and is worth reading about on its own: The New Enemy Problem.
JWT Scopes Can’t Provide Fine-Grained Authorization
While the original claims for JWTs are well specified, everything else is actually quite murky.
Remember that OAuth2
It’s actually quite vague.
The specification where
scope is defined is actually in yet another spec and it doesn’t offer much additional context other than some requirements around what characters form valid strings.
If you read through all of the specifications, you’ll find examples of values for this claim like
email profile phone address.
At a glance this might seem reasonable; it’s listing properties on a user’s profile.
Developers typically add some more context (e.g.
profile:admin) and call it a day.
However, this still lacks context.
What data exactly are we talking about here?
The whole website?
Only the current user’s profile?
You can easily find high-profile web apps out in the wild that have done this; it has plagued GitHub’s REST API for years!
Modern applications need Fine-Grained Authorization down to individual items (e.g.
issue/authzed/spicedb/52:author instead of just
Considering users can have access to thousands, millions, or even billions of software objects; it is unrealistic to think they can all fit into a token that is passed from service to service over a network.
On the other hand, centralized authorization keeps any data required for authorization in one (central) location and allows services to query specific, granular, and up-to-date authorization data on-demand. By decoupling the data, you are free to scale your authorization system as needed. SpiceDB specifically is built upon a ReBAC (Relationship-Based Access Control) foundation that can effectively scale to extraordinarily fine-grained data while also being powerful enough to model popular design patterns such as RBAC, ABAC, and User-Defined Roles. If you're now scratching your head wondering how fine-grained your data model should be, we've got a whole post on it.
JWT Authorization Requires an Oracle to Predict Necessary Scopes
Let's pretend that you could get away with using very few JWT scopes. You'll eventually hit a wall, but let’s just do so anyway for the sake of argument. How do you know what scopes need to be in your JWT for a given request? JWTs are often created by an ingress or API gateway at the beginning of a request flowing into a service-oriented architecture, so that means the creator of the JWT needs to know exactly what services are going to be downstream of the request and what permissions they might require. This is impossible to predict for anything outside of very simple systems, but if you’re already split into microservices it’s very unlikely that your model is simple.
Additionally, any token with more scopes than necessary that is passed along to the next service is a huge target for attackers attempting to escalate privileges and gain access to sensitive data. This exact problem led to the creation of an alternative token to JWTs called Macaroons, which one of the AuthZed engineers, Evan Cordell, built for the Python ecosystem. Macaroons allow for tokens to be attenuated to reduce their scope before being shared with the next service. Sounds great in theory, but in practice, properly using Macaroons is so complex that there hasn’t been much adoption and those that have adopted them often argue it wasn't worth it.
Centralized authorization systems acknowledge that it's impossible to know ahead of time all the permissions a request will need in order to be processed. Instead, they allow for ad-hoc queries when necessary. This does have overhead of reaching out to the centralized system, 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.
When are JWTs for Authorization Actually Recommended?
After reading all this, you might be thinking that there really isn’t any time or place for JWTs! However, there is one recommendation that is often repeated: one-time grants where access cannot be revoked. I'm convinced that this is an exceedingly rare situation, nonetheless, it is a valid one.
If you'd like to learn more about topics like ReBAC and centralized authorization, feel free check out the rest of our blog.
- What is Google Zanzibar - Jake Moshenko
- New Enemies - Jake Moshenko
- Zanzibar Implementations - Sam Kim
- Google Zanzibar Through Our Eyes - Jake Moshenko
- The Architecture of SpiceDB - Jake Moshenko
- Caveats: A Scalable Solution for Policy - Jimmy Zelinskie
- How small is too small? - Jake Moshenko
For additional content covering JWTs that’s mostly (but not exclusively) about web sessions, I recommend the following: