Have you ever scaled out a web application or an API? If so, you'll have noticed your out-of-the-box web framework stores its user session in memory. You may have utilized sticky sessions in response, or stored your sessions in a shared database---but these solutions aren't without problems. For example, in a shared database, you'd need to make an extra request to the database for every HTTP request.
To account for such flaws, using a form of stateless authentication allows for better application scalability, as well as more efficient authentication of HTTP requests. Token-based authentication is perhaps the most common stateless authentication strategy, and JSON Web Tokens (JWTs) are today's token of choice.
While using JWTs for authentication is extremely common, some application developers are building authorization and permissions directly into their web tokens, risking serious security implications as a result. Baking authorization directly into JWTs also comes with several limitations that should be avoided.
Data inside of JWTs are generally not encrypted, leaving them human-readable. While this lack of encryption is by design, it comes with some serious concerns around storing private, sensitive, and authorization-based information in JWTs. This is especially true when your organization is under certain regulatory compliance like HIPAA or SOC 2.
In this article, you'll learn why application developers should avoid using JWTs for authorization.
JWTs are like a key. A web application gives a client a token, which the client then uses to access protected resources. JWTs are composed of three parts: a header, a payload, and a signature.
The header contains meta-information about the JWT.
The payload contains the information used to identify the owner of the token: user ID, email address, etc. These are called claims. A standard set of claims exists to ensure collisions of claim names are avoided. That being said, JWT payloads can hold custom claims. Essentially, this means they can hold whatever information you may need.
The signature is what makes a JWT secure: it's a way to verify if a token was created from the same issuer, and ensure it hasn't been tampered with.
JWTs are usually not encrypted. The information within the header and payload is encoded (not encrypted), which means it can be decoded. The key to keeping JWTs secure is ensuring the header and payload are hashed using a secret, which is similar to hashing a password. The resulting hash is the signature that's part of the JWT.
Any party with access to a secret can use it to hash the header and payload values it receives and verify that the signature matches. If the hash and token signature don't match, it means the JWT header and/or payload were tampered with.
Token-based authentication is when you use a JWT (or other token technology) to verify the identity of a client.
One of the most common benefits of using a JWT for authentication is not having to fetch user data from a database; you already have the essential user information embedded directly in the token. Whenever an API receives an HTTP request with a valid JWT, it extracts the user's information and uses it as needed.
This is why we call JWTs stateless: the API's server doesn't need to keep track of a user's session state.
Similar to using JWTs for stateless authentication, JWTs can also be used to store authorization-based information like user permissions and roles.
Storing a user's role in a JWT means you won't need to query a database to know whether a particular JWT is permitted to access certain resources. Based on this, JWTs can provide performance and scalability gains when it comes to authorization.
As a software engineer, you often have to make trade-offs between security and performance. For example, modern password hashing techniques often sacrifice speed for added security. These techniques utilize high hashing iterations, avoiding timing attacks by using a constant equality operation to compare hashes.
While storing authorization information in JWTs offers significant performance benefits, serious concerns emerge as a result. Let's break down some of the most pressing:
With JWTs, all payload data is visible to the parties who have token access; to reiterate---they aren't encrypted, but encoded. Authorization information may be considered sensitive information for a particular system, especially when your software systems are under compliance and governance restrictions.
For example, personally identifiable information such as a person's name, email address, or phone number are all considered protected health information under HIPAA. If you're storing this kind of data in JWTs while under HIPAA compliance, you could have a major security and compliance risk on your hands depending on where your JWTs are being used (internal network vs. public client).
Note: There are techniques that can help protect user information tokens, like using phantom tokens. However, this makes using JWTs much more involved and complicated.
Imagine a token for a customer named Bob has a payload identifying him as an
admin. It's discovered that Bob is a bad actor (a spy or an upset employee). As a result, you disable Bob from inside your web application. Except... the JWT that Bob is using from his mobile application hasn't expired yet.
Bob can keep issuing requests until the JWT expires. Since authorization is baked into the token, you aren't querying the database to get the most up-to-date information about Bob's role or permissions. Bob can continue to do damage to your system---even though he's supposed to be disabled.
There are two common ways to avoid this scenario:
But these "solutions" come with their own issues.
Short-lived tokens don't necessarily solve your problem. It wouldn't take long for a disgruntled and disabled administrator to harm a system---even less than five minutes. This can also cause user experience issues for clients; they would have to refresh their access token more often, or sign-in to the service again. Negatively impacting user experience is something you should always try to avoid.
Storing valid tokens in a database so they can be revoked destroys the main reason for using JWTs in the first place---to avoid unnecessary trips to your database.
One of the best ways to mitigate this is to store user roles and permissions in a dedicated access control tool like Cerbos. You could also store user permission information in an efficient storage system like Redis.
JWTs are much larger than the standard session cookies that a web application would typically use. Once you start adding more data into a JWT's payload, its size may cause HTTP requests to perform slower.
HTTP headers have size limits too, depending on the web server stack being used.
Once you go beyond the basics, implementing JWTs is complicated, increasing the chances of developers making mistakes as a result. For example, even Auth0---one of the sponsors of JWT.io---had a JWT vulnerability with their API back in 2020!
Similarly, improperly configuring your JWT by using a third-party library or service could allow someone else to attack your system---something you want to avoid at all costs.
JWTs are not a cache---they shouldn't be treated as a caching technology. It can be easy to shove as much information about a user and associated resources into your tokens as possible, but that doesn't mean you should.
Again, this data is not hidden to anyone who can see the JWT. All data in the payload is stale, which can affect the performance of HTTP requests.
Much like the issue with stale data in JWT payloads, you should use an efficient caching technology to cache data, or a dedicated access control service like Cerbos.
JWTs are supposed to help you avoid making extra database requests to fetch user information. Despite this, it's routine to query your database to fetch extra user information.
Any non-trivial piece of business logic will need to know details about a user: what department they're part of, special mappings to resources in your system, special business rules that can't be expressed by a mere data attribute, etc.
It doesn't make sense to bloat your JWTs when you're going to need to fetch that same information (and more) anyway.
JWTs can be a great way to authenticate clients, giving them a way to access protected resources by using a short-lived token as a "key". When it comes to making distributed services more scalable and efficient, JWTs shine.
However, when it comes to storing authorization details, JWTs come with a number of risks. JWTs come with the added cost of larger HTTP requests, negatively impacting HTTP performance. Depending on your traffic, this could cause a significant increase in the associated usage costs of your cloud services, slower endpoints, and the risk of blowing past HTTP header size limits.
In addition to performance issues, JWTs face regulatory compliance difficulties, as well as expiration problems which leave you open to system attacks. Plus, you'll probably be fetching user information from a cache or database anyway.
This is why a dedicated access control service like Cerbos can be a better option. Cerbos' ultra-fast API is designed to make access control decisions within milliseconds, while providing a simple way to configure them. To learn more, check out Cerbos.dev.
Book a free Policy Workshop to discuss your requirements and get your first policy written by the Cerbos team