Why you should not use JWT
October 26th, 2021
In this article:
How do you handle user authentication in your web app or API? When it comes to implementing auth, JSON Web Tokens (JWTs for short) are often touted as an industry best practice. On some platforms, and for some frameworks they are the first thing that comes to mind: we've seen many discussions on developer forums (such as /r/node) where the only alternatives suggested were JWT if you're doing it yourself, or using a 3rd-party service such as Auth0.
When we added Node support to API Bakery, we thought long and hard whether to include JWT, because of its popularity. We decided against it. Here's why, and why you should probably not use JWT either.
Crash course on web auth
Most of the client-server communication on the web nowadays is stateless request-response (WebSockets are super useful, but people are not replacing plain old requests with them). This means that after your user logs in (for example, by providing email and password to your server), you, as a web developer, need to think how the next time that user's request hits your server you'll know it's from the same user.
The first popular mechanism was cookies - arbitrary key-value pairs the server sends to the client. Client sends the same cookie key-value pairs back on following requests. Server can then inspect the cookies received and figure out information about the client.
Sessions were built on top of cookies. The server kept context about the user's "session" (details about the user, shopping cart if the site was a webshop, and so on) and indexed it with a random session ID. The session ID was sent as a cookie and for each subsequent request, server looked up the session state. For the most part, this worked well, especially if your web framework of choice kept you safe from problems like CSRF.
What are JSON Web Tokens
JSON Web Tokens (JWTs) are a type of token with a twist. Instead of a random unguessable string (a bearer token we just described), JWT contains all the information about the user directly in itself. The information is cryptographically signed so a malicious user can't just change a few bytes in the token and the server will immediately see the signature doesn't match the forged contents. The information itself is a JSON-encoded object, that's optionally encrypted, then encoded and signed in a specific way, all defined in RFC 7519, an open internet standard.
Major advantage of JWT compared to bearer tokens (or indeed, session authentication) is that they don't require looking up the token. If you have a distributed system, each node in the system can verify JWT correctness for itself and immediately use the data - no need to look up the random string in the database to figure out who it is.
That's great if you've got a true microservices architecture. Your auth service can check user credentials (email and password, or using a 3rd-party auth provider), issue a JWT and call it a day. Other microservices can independently verify the correctness of the token and have the user information immediately, without the need to check in with the auth service. This shaves precious time off of every request handling and lowers the load on your auth service. What's not to like?
Unfortunately, a lot.
Problems with JWT
An old joke is that there are only two hard things in computer science: cache validation, and naming things. JWT is named pretty well, but fails miserably at the first problem: invalidation, or How do you log out the user?
The answer is, you don't. You can't. You (the server) can tell the user's client software to forget their JWT and hope they'll do it, but you can never be sure. Well, you could keep a list of tokens that are no longer valid - that is, the user has logged out and the token should be ignored. However, on each request, you need to check the token against this list and you've just lost your only advantage over much simpler bearer tokens, so that's not gonna work.
Another strategy, and what people usually do in practice, is to minimize the damage: keep the token lifetime short (a couple of minutes) and issue another token, a refresh token. When the user logs in, you issue a short-lived JWT and a long-lived refresh token. When JWT expires, the client must request a new JWT using the refresh token. Your auth service then checks that refresh token is still valid (which is less of a problem as it's not happening on every request) and if it is, issues a new JWT.
Issue solved, right? Well, kind of, except you've just reintroduced a bearer token, because that's exactly what the refresh token is. Looking at JWTs from that perspective, you've introduced a client-side cache of user identity (the JWT) and added a bunch of complexity (involving the creation, verification, and token refresh) for the hope of optimizing part of the work you used to do on the server (checking user identity using a bearer token). Was it worth it?
And that's just the biggest problem. We've ignored things like increased attack surface due to complexity of JSON parsing/validation, misconfiguring JWT libraries to allow no signature, leaking data to users because you thought JWTs were encrypted (they're usually not by default), and poor implementation of API clients that don't properly re-run requests that failed due to expired JWTs. The list goes on, and on, and on ...
Who should use JWTs
There certainly exist cases where JWT might be a good fit. If you have a truly distributed microservices architecture where the services themselves are completely independent (no touching the central database, or even a concept of a central database), then look up JWT. Or even better, some of the new JWT look-a-likes that fix some of its glaring security problems, like PASETO.
Who should not use JWTs
Everyone else, and that means most of web developers out there, should avoid them.
What should you use instead? To quote Thomas again from the same article: I continue to believe that boring, trustworthy random tokens are underrated, and that people burn a lot of complexity chasing statelessness they can't achieve and won’t need, because token databases for most systems outside of Facebook aren’t hard to scale.
How we do auth
This brings us back to how we implement authentication in Node projects you generate with API Bakery. If you've read all of the above, no surprise here - we use bearer tokens. They're simple to implement and simple to understand. For many projects, they'll be just enough. And if you do need another mechanism, the auth code in the generated project is isolated enough that it won't be a problem to rip it out and replace it with something else.
But, don't use JWT.