McElfresh Blog

Go, PostgreSQL, MySQL, Ruby, Rails, Sinatra, etc.

User JWTs vs One Encrypted Secure Cookie

Posted at — Dec 26, 2023

Overview

JWTs are used in a variety of contexts, but we’re going to focus on JWTs and secure cookies, used in browsers, to store user information in the browser, and provide user authentication and authorization to server-side resources.

In my experience, JWTs are often credited with providing security and performance they do not provide. They are called “stateless”, meaning that they store state on the client, and not on the server. However, most, or perhaps any, reasonable implementation requires that JWT state be maintained on the server as well, in order that JWTs may be invalidated.

Morever, JWTs are often used to store personally identifiable information (PII) that they should not store. Such PII can be scooped up and used in a number of ways that aren’t good for the user.

JWTs Are (Almost Always) Not Stateless

JWTs are said to be “stateless”. The logic goes like this: Once a JWT is issued, it is valid until either 1) it expires, or 2) the key that checks its validity is invalidated. The state of any particular JWT does not need to be stored on the server, because, once the JWT is created, it carries its own authenticity and authorization. When the server sees a JWT, it can check it was correctly issued, and that all the data inside it is correct.

JWTs typically keep state in the browser cookies, local storage, and / or session storage. A typical JWT might describe this state: “This user has id 123, username “Charlie”, and is valid for one hour.”

JWTs are usually created with an expiration, and this expiration is often used to limit access to server side resources. By itself, this is provides no security at all.

The only ways to invalidate a JWT that has not yet expired, are to 1) swap out a server’s private key, which will invalidate all JWTs, usually not the best option, or 2) add particular JWTs to a blacklist. A blacklist requires a server side lookup, not only for blacklisted JWTs, but for all JWTs. Once we have a blacklist, our JWTs are no longer “stateless”.

JWTs Are Often Used to Store PII

Cookies, local storage, and session storage are often used to store information between browser requests. Often, this data is stored inside JWTs. This data includes things like:

Storage of database ids, in local storage, or in cookies, is of course not limited to JWTs. You’ll find database ids littered throughout your local storage and cookies, for all kinds of websites. Database ids that could now, or anytime in the future, provide clues to hackers on how to access resources in your site, should never be stored in the browser.

Once your “stateless” JWTs have been made stateful, by requiring a blacklist check, secure, encrypted cookies become a simpler and more powerful option. Secure, encrypted cookies are stateful – they store user state on the server. But JWTs + blacklists are also stateful, and require more machinery – signing / encryption + signature checks / decryption – than secure encrypted cookies do.

A “secure cookie” is one that will be sent to the browser, and back to the server, only over https. This takes care of encryption in transit.

For encryption at rest, I prefer to 1) encrypt my cookies, and 2) store no unencrypted PII on the user’s browser. Here is my preferred cookie implementation:

Of course, there will usually be database ids in URIs, and this is often ok (products/1). But user_ids should never be readable in plain text, anywhere but on the server. And, although there will be lots of information stored in the browser page cache (browser history, PII, etc.), the page cache is not readable by javascript, is not hackable via cross-site scripting. Data is safer in browser caches than it is in local storage / cookies.

Conclusion

Client-side JWTs that require blacklists (in my view, is all of them!) aren’t “stateless”. Moreover – I think because the server can verify their contents have not been tampered with – somehow, this false sense of “security” gives rise to adding, in plain text, information, like user_ids, that JWTs should not contain.

Secure, encrypted cookies have proven a better alternative for me, for web clients.