you may be thinking to yourself: “I should go re-write my authentication layer to use it.” Before you dive right in, you may want to consider some of the security implications that JWT introduces.
As context, the Prevoty engineering team is currently in the process of re-writing our management console. Recently, an engineer proposed JWT as a solution for handling our sessions client-side - rather than storing and managing them on the server. After weighing the options, we’ve chosen to not implement with JWT. This post will explore the reasons and motivations why we opted not to.
JWT (pronounced “JOT” and short for JSON Web Token) is a relatively new token-based assertion system that has been receiving a lot of press as of late. Without getting too deep into the details (you should read the RFC3 for that), the basic idea behind JWT is:
You generate a claim of arbitrary JSON data
You decorate it with some metadata, such as when the claim expires, who the audience is, etc.
The data is cryptographically signed with a Hash-based Message Authentication Code (HMAC)
The header, data, and signature are then Base64 encoded and concatenated together with periods to delimit the fields, which results in the token
On the surface, this looks sane. We have a token that allows us to convey an assertion to an application, where data (encrypted or not) can be stored client-side and the server can trust it by verifying the cryptographic signature. By providing anti-tamper protection via HMAC, it is secure as long as the cryptography holds up.
SO...WHAT'S THE PROBLEM?
The problem lies in the ways engineers are using these tokens for their applications. JWTs make sense in an environment where the user and attacker are not able to play in a giant programming sandbox. They’re great for replacing OAuth solutions for communicating between services, storing authentication tokens for mobile devices, or when performing API requests. The key here is that they are stateless. These should not be used to replace sessions in your shiny new HTML5 app.
Traditionally, sessions are implemented server-side. When a user logs in, a random session identifier is generated, persisted in a data store, and the client is told to set a cookie with the corresponding ID. When a request is made, the server looks up the session associated with the ID and retrieves the data.
Now you might be thinking: “...but JWT sounds perfect here. You can store everything client-side without worrying about where to persist the data or dealing with the pesky issue of sharing session data between multiple instances of an application.”
On the surface, you’re right. However, what happens when you want to log a user out of an application? When you deploy an update to the application and want to invalidate current sessions? When you’re updating sessions as data changes? When you’re storing sessions?
Let’s say a user is ready to logout after a long day of posting comments on the cat pictures hosted on your website:
When the user signs out, a DELETE request is (hopefully) fired off to the application
The application responds with, “Cool! Time to destroy this session.”
To clean up, your application may null out the session or remove the persisted value from the data store. The result is the same; no more session.
The application might do something clever, like send down a new ‘Set-Cookie’ header in the response telling the client to replace the old session ID with a blank one. But really, who cares? The session is gone.
If the client attempts to use it again the server will respond with “Sorry, don’t know about that one,” and hopefully send back a nice 401 Not Authorized or 403 Forbidden.
There are many options for destroying client-stored sessions and they all depend on the storage system utilized. It’s important to note that there isn’t really a way for the application to destroy a session. It can suggest that the client remove the session from wherever it is stored, but client-side security is not foolproof or reliable. If the DELETE request is ignored or if an attacker steals the data, the session will magically still exist. One small note: JWT does provide an ‘exp’ (expiration) field to specify when a claim expires, but if the server were to push down a new token and change its expiration time to now, the client could simply ignore it. All prior tokens would still be valid. In order to safely delete a session, the server must keep track of client-side sessions regardless.
Similarly, what happens when you roll out that sweet new virtual cat simulation feature you’ve been promising your users for months? As part of the implementation, you’ve added some additional data to the session and all existing sessions have been invalidated. Maybe you decide that you really want to upgrade sessions to keep users logged in, so you add special logic into the code to perform rolling upgrades. Hopefully the code will make its way out of the code base within 6 months -- after a significant portion of the users have upgraded their sessions and accessed the new virtual cat simulator. Over time, this code will become cruft for some unfortunate engineer to maintain. Instead, you could go with the easy solution: invalidate all current sessions and have everyone log in again like the rest of the internet...but wait – with JWT, there’s no way to destroy sessions server-side!
We’ve touched on invalidating sessions but what about updating them? In a normal application, we’re unlikely to have a lot of session data changing frequently after login. When it does, we’ll have to send down a whole new session again. Let’s talk a little about size. Right off the bat, we know a bit about the structure of a JWT:
We know there’s a minimum sized header, the claim (which contains some metadata) plus whatever actual data we want to store, and we have a fixed-sized signature
When we Base64 encode all these values we’re talking about a ~37% overall increase in size
The smallest encrypted token we generated (without any data in it) was ~450 bytes. This may not seem like a lot when you consider the size of the rest of the content, but it does increase the overall bandwidth required for a request/response life-cycle.
Lastly, when looking at the size we must also consider storage. There are a couple options, but we’ll first discuss the method everyone is recommending. All the articles we’ve seen thus far have discussed storing tokens in local storage. Nothing against local storage — I actually think it’s a pretty awesome idea — but it’s not a good solution for storing your authentication data by design. Just don’t do it.
Applications have been setting session IDs in cookies since the old days of table layouts and under construction GIFs.Cookies haven’t always been this nice, relatively secure system for storing data. When AJAX first came onto the scene and before the SSL everywhere movement, cookies were like the hippies in the 60s; they were all about freedom. They lived fast and loose and were happy to let anyone get their hands on them. Eventually, some smart engineers thought, “You know what, we should make these more secure,” and introduced the ‘secure’ and ‘HttpOnly’ flags for cookies.
If you don’t know what these handy little flags are, gather ‘round and I’ll tell you stories of the bad old days, having to walk uphill both ways to complete my 3-way handshake. When I was a kid, we didn’t have these new fangled SSL downgrade attacks or mixed content warnings; we were lucky if we even had encryption to begin with. Some smart attackers realized mixed content was bad and could be used to steal sensitive data. Sure, the page you’re requesting might be encrypted but that cat picture you’re pulling in? It isn’t. The domain is the same and it only means one thing : your browser -- being the helpful kind of application it is -- will send along that cookie in all its plain-text glory for Eve to see! Those smart engineers added a handy little flag that an application could specify in the cookie that would tell the browser, “Whoa! Hold on there. This connection isn’t secure, better not send that cookie.”
That’s it, boys and girls. Story-time is over. Back to our discussion of storage.
All that said, JWT may look like a fairly sane method for making trusted assertions but please don’t be so quick to throw away years of security enhancements for new, shiny tech. Understand the risk and limitations accepted with opting for client-side sessions before adopting them for your application. Client-side sessions aren’t always wrong, but there’s a high probability that they’re not suited for your web application.