Generally usage begins with an identity provider authenticating a user through traditional means (password, certificate, multi-factor, etc.). The provider places information about the user in the payload along with standard fields such as when the token was issued and when it should expire. The provider then uses a secret key to create the signature for the payload, base64 encodes the three sections, and returns the token to the user. When the user wants to prove their identity, they submit the token to the server, which may be the same server that created the token, but could be any server. The receiving server uses a key that has been pre-shared by the identity provider (if it’s a different server) to recompute the signature based on the payload received, then compares the computed signature to the received signature. If the two match, then the receiver can trust that the token was created by the identity provider and that the payload has not been modified since it was created.
Planned Benefit 1 - Standardization
Any protocol is fundamentally a definition of what certain things mean so that two parties can communicate more efficiently. Having a standard for tokens that is widely used across the internet is useful in a variety of situations. For starters, languages and web frameworks can build libraries to support the protocol that make usage simple for developers and can be widely tested to ensure they are correct and secure. From there, single-sign-on providers can use the protocol to issue tokens that will be easy for a website to verify. Even if the issuer and receiver are the same site, that site is relieved of the burden of creating their own token system which may or may not end up being secure. Standard protocols are a good thing.
Planned Benefit 2 - Compactness
The RFC claims that JWTs are compact and depending on what you are comparing it to, that could possibly be correct. However, if we jump ahead and start comparing it to the traditional “long” random value used as a token in cookie implementations, the traditional token is almost always going to be smaller. On the other hand, data transfer rates are fast enough that almost no one cares. To quote an author on the IETF JOSE discussion list “I doubt people seriously golfing bytes are using JWTs” (https://mailarchive.ietf.org/arch/msg/jose/VN9NRGr3nk78PhlNM1XkWKHfLNE/). For our purposes we’re going to forget the claim was made and move along.
Planned Benefit 3 - Self-containment
The big selling point of JWTs is their “self-containment”. Because the payload can contain an arbitrary amount of actual data, it can hold everything that might normally be placed inside a database. So while a traditional token requires the server to make a database query to look up the session data based on the session token, a JWT allows the server to simply decode the payload, verify the signature, and then trust the data directly. On truly massive sites with hundreds of thousands of simultaneous users, constructing a database that can provide prompt responses to all of those queries can be very difficult, so skipping that step could produce a significant performance gain, at least in theory.
The problem comes in step 2. If the only thing you truly need to know about a user to fulfil the request is that they are authorized and maybe their role, then a JWT can reasonably provide that. But as soon as you need to start providing customized site content, you are probably going to be making database requests that include the user’s identity anyway. A shopping website could conceivably put your whole shopping cart inside the JWT, but they shouldn’t be putting your address and stored payment credentials (even tokenized), in it. Now there may still be performance gains if a user can do most of their shopping without a session database and only make the queries for payment and shipping details at the end, but, the design of the site needs to be modified from what developers have done in the past in order to achieve these gains.
The reality is the performance gains achieved here (local database query time minus the JWT signature calculation time) are measured in milliseconds. If your site has such high performance requirements that milliseconds matter, then JWTs might be a tool for you. On the other hand, the vast majority of websites on the Internet these days are not measuring their page load performance with more precision than a second, if at all, and none of the biggest websites I could think of (GMail, Amazon.com, Netflix) are using JWTs as their authentication token. In fact the same architectures that trend towards using JWTs as session tokens, trend towards web-API based designs that make dozens of individual requests as different components of the page are loaded in. The performance hit of a cross-Internet network request is going to far exceed a local database query. So if the argument for JWTs is performance, it needs to be accompanied by other high performance design choices to be believable.
Minor Security Concern 1 - The Logout Feature
Logically, self-containment is in direct conflict with server side enforcement of sessions. Some session controls, such as the timeout can still be enforced using JWTs by setting and validating the expiration timestamp. A simple JWT with a short expiration time is similar to an idle timeout since with no further action from the user the token will stop being valid. Some sites also use a refresh token which can be used to generate a new simple token. The refresh token has a longer expiration time, similar to the absolute timeout, and from a security perspective, the user should be required to reauthenticate to get a new refresh token. The control that cannot be implemented is a secure logout feature. Sites can still have a logout button that causes the browser to forget the tokens and as far as that browser is concerned, the user is logged out. But to the server, the token (simple and/or refresh) is still valid so if an attacker has stolen the token, they can continue to use it for its full original lifespan. If we take the same critical eye to this risk that we took to the benefits, then honestly speaking, the increased risk is pretty small. There are more important controls that should prevent the token from being stolen in the first place, and any time a token is successfully stolen, things are going to be “bad”. Limiting the lifespan of a token is a way to make things “less bad”, and since users rarely actually click the logout button anyway, the benefit, or lack thereof, from server side logout is even smaller. But there is some benefit to be had.
Minor Security Concern 2 - Data Exposure
One consequence of the payload of a JWT being encoded rather than encrypted is that anyone who has access to the token can read its contents. What a malicious user or attacker can do with that payload depends on the website and if it otherwise has no vulnerabilities, the impact will end up being zero. However, “sometimes’ the data exposed in the JWT is useful to an attacker, and when testing a website I will always decode the JWT to see what I can find. In some cases it is simply the user identifier which is generally leaked other ways. Sometimes it is a list of Windows Domain Groups that reveals internal organization structures and may be useful when paired with an Internal Penetration Test. Sometimes the whole user object is dumped into the token. In at least one case, the name of the user attribute controlling the role enabled me to perform a mass assignment attack to change my role. I probably could have guessed the value, but having it spelled out in the JWT made my life as an attacker easier. Developers should never make an attacker’s life easier unless there is a tangible benefit to the application.
Conclusion
Taken all together, the decision to use JWTs generally comes down to theoretical benefits that aren’t actually realized in a specific site versus minor security concerns that only matter in certain situations, but since it is difficult to know if you are in one of those situations, the risk calculates to a non-zero value. This means that for most sites on the Internet, JWTs end up being a slightly worse solution for session management than traditional random strings.
If I were a CTO, CISO, or Chief Architect, I would document somewhere that unless a technical need can be demonstrated (likely in the form of performance or interoperability with an external site) that server-side session management SHOULD be used over JWTs. If an application design using JWTs came to me before it had been implemented, I would question the developers on the choice and likely recommend they modify the design. However, if an application came to me after implementation, the risks associated with JWTs are so small I likely would only ask the development team to rework the solution if it required less than a couple of days development time to do so. Individual risk tolerances will vary, but for my money, JWTs are a demonstrably worse solution to session management, but by a small enough margin that the fix is often worse than the risk.
Related Discussion 1 - Implementation Errors
Astute readers may have noticed that I did not call out the risks associated with not verifying the signature of the JWT or tampering with the first section of the token to trick the receiver into trusting a signature they should not. While these vulnerabilities were certainly observed during the early days of adoption, by my experience they were not as common as some people would claim. As standard libraries became readily available and developers didn’t have to “roll their own” JWT support, implementation mistakes such as these have become more and more rare. Again that risk is technically not zero, and could be used as further justification not to use JWTs, but it seemed more fair in this treatment of the standard to focus on the fundamental risks as opposed to how individual implementations may have issues.
Related Discussion 2 - Authorization Header vs Cookie
This article also attempted to avoid conflating cookies with server-side session management (using long random strings). A JWT can be stored as a cookie, and a random token can be sent in an Authorization header, though it does seem that many JWT based authentication schemes opt for the Authorization header solution. From a security perspective Authorization headers are automatically resistant to Cross-site Request Forgery (CSRF) attacks, because, based on the the Same-origin Policy, browsers will not allow JavaScript (or any other source) to create requests that contain customized headers destined for any other site than the one currently being viewed. However, in most cases having the value needed for the Authentication header accessible for the legitimate code to send means that it is exposed within the JavaScript context and becomes vulnerable for reading through a JavaScript injection (Cross-site Scripting) attack if such a vulnerability exists. Techniques such as storing the token only in an isolated worker thread can help defend against this. In contrast, by default a cookie is sent along with any request destined for the site defined by its scope, whether that is generated by a simple link, a form, or a JavaScript XMLHttpRequest method making it the traditional mechanism for CSRF. Cookies can easily be marked with the HttpOnly attribute to protect them from JavaScript injection attacks. Cookies can also have the “SameSite” attribute set to “strict” which will apply the same Same-origin Policy restrictions to it. In short, defenses exist to protect both cookies and tokens used in authorization headers from attacks. Those for cookies can be enabled simply by setting attributes which makes them easier to implement, but either can be used safely.
Other articles in this series
Part 1 - Overview
Part 2 - Network Sniffing
Part 3 - Token Exposure
Part 4 - JavaScript Injection (XSS)
Part 5 - Blind Session Abuse
Part 6 - Post-compromise Use
Bonus 1 - JWTs: Only Slightly Wors (this article)
Bonus 2 - Device Bound Session Tokens
Errata
See a mistake? Disagree with something?