JWTs Aren’t Made for Authorization
- Share:
If you’ve been involved in software development in the last decade, you’ve probably heard about JWT (JSON Web Token). JWTs have gained popularity among developers due to their compact size, URL-safe format, and ability to enable stateless verification of user identity.
With their popularity in various types of applications, the JWT has also been misused by many developers as an authorization solution. While JWTs indeed play a crucial role in authorization, this misconception has the potential to introduce vulnerabilities and flaws in application security.
In this blog, we aim to understand how this misconception came to be, how to use JWTs properly, why you shouldn’t use them for authorization, and what you should do instead.
Before we dive in, let’s do a quick recap of what JWTs are -
What is JWT?
A JWT, or JSON Web Token, is a compact, URL-safe token used to represent claims between two parties. It consists of three parts:
- Header: This contains metadata about the type of token and the cryptographic algorithms used to secure its contents.
- Payload: This is where the actual data or claims about the user or other entities are stored. These claims can be predefined (like
sub
,iat
,exp
) or custom attributes relevant to the application. - Signature: This is created by taking the encoded header and payload, concatenating them with a secret, and applying the specified cryptographic algorithm. This ensures the token’s integrity and authenticity.
JWTs are primarily used in authentication scenarios to verify the identity of a user and can carry information about the user to the backend without needing to query a database.
Why are JWTs so popular?
Three components (They’re in the name) make JWTs so popular with authentication solutions: their use of JSON, their decentralized web-based architecture, and their token-based security and verifiability.
- JSON: The Internet and the way applications communicate with each other are based on a JSON data structure. It’s commonly found and comfortable to use, and its tree structure makes communication between applications easier. Every JWT, at its core, is a JSON structure of a user that includes some information about them. There are, of course, many uses for JWTs. They can be used as API declarations, ID tokens, and access tokens, with access tokens being the most popular of uses.
- Decentralized Web-Based Architecture: A decentralized architecture, as the name suggests, means that many different components must communicate with each other. The great thing about JWTs is that their verification supports a decentralized architecture, allowing you to efficiently connect your identity provider, authentication provider, and application.
- Token-Based Security: The way JWTs are built means users can verify them without being dependent on the entity that created them. Don’t get me wrong - you still need some information from the JWT provider, but you don't need to call them to verify every JWT.
OAuth
Another reason JWTs are gaining popularity is the OAuth standard, which is currently the de facto standard for authentication. The term OAuth itself can be confusing, as it stands for “Open Authorization” while being an authentication standard. The basics of OAuth involve using multiple types of tokens, but the main ones are ID tokens and access tokens.
There are two ways to verify tokens: The first is to call the token issuer and ask, "Is this token actually valid?" The second, which is more popular due to its decentralized approach, is to look at the token itself and understand if it is valid and verifiable. This approach is possible thanks to the structure of JWTs.
As we covered previously, JWTs consist of three parts: a Header, a Payload, and a Signature.
The signature of the JWT allows us to verify this token by using JWKs (JSON Web Keys).
This means we can verify the token regardless of whether the issuer is available, allowing us to validate the tokens themselves even if we are offline. By validating the tokens, we can have a very secure decentralized way to create an OAuth architecture based solely on tokens. Thus, JWTs have become popular because they helped OAuth grow efficiently.
What Should JWTs Be Used For?
JWTs are designed to securely convey claims between two parties. In the context of OAuth, this typically means using the access token to determine if a user is authorized to access an API. However, JWTs are not designed for fine-grained permission checking (We’ll get to that in a second).
JWTs are especially useful for stateless, distributed systems due to their self-contained nature. They allow the receiving party to verify them without needing to refer back to the issuer, making them ideal for authentication scenarios like OpenID Connect (OIDC), where they can securely convey user identity and claims.
While JWTs can extend the functionality of your authentication provider, their primary role should be for coarse-grained authorization, such as verifying if a user can access a resource. They are not intended to manage detailed permission control or complex access scenarios.
Bottom Line - JWTs are best used for:
- Stateless, distributed systems where self-contained tokens are beneficial.
- Authentication purposes, such as those defined by OIDC.
- Coarse-grained authorization to protect access to APIs.
What JWTs Aren’t
Once a user is logged in and the authentication process is finished, we move on to authorization - deciding what a user can or cannot do in our application. At this step, the only verifiable thing we have is the user's identity based on the JWT.
The main component of JWTs is their payload, which consists of a user ID we can identify. Because authorization was highly tied to authentication, users kept storing increasing amounts of data in this payload.
To make things easy and not put in much effort, many developers decide to retrieve attributes in the JWT and pair them with imperative code in the application. This often results in a statement like “If the JWT role equals something, allow/deny something”. Sometimes, when they want to create something more dynamic, they add an external function to fetch roles based on the JWT.
This is the result of many developers seeing the JWT's verifiable nature as something that fits verifying permissions.
As we previously mentioned, most developers first encounter JWTs when they need to handle user authorization. Most developers are not really aware of the verification process, which concludes with the authentication service providing you with a JWT.
At this point, as the JWT can carry some basic scopes and claims that define permissions in our application, many developers tend to think, “Now that the user is verified, I can use this token to define what access a user should have”.
While roles and claims play an important part in our application's permissions, they aren’t sufficient to fully define them. This approach of looking at the JWT as an authorization component is problematic for two reasons:
- While JWTs are one of the greatest developments in software development in the past few years, they are not intended for authorization.
- Using JWTs as the only method of managing user access within your app can be potentially disastrous for your application's security.
Let’s dive deeper into those issues:
Why shouldn't you use JWT for authorization?
The first thing we need to understand about JWTs is their revoking mechanism. As mentioned previously, JWTs are the basis for token-based authentication. When we manage a user session, we can say, “Now that we have the JWT, we know that the user has been authenticated and verified, so we can reuse this to authenticate them”. This means that our authentication is stateless. We don’t need a session that says when it started and when it ends. If a JWT expires, we can just ask for a new one. This is how OAuth works - as it exchanges tokens, we are able to ask for a new JWT from our OAuth server or any other kind of authentication token exchanger.
JWTs are Static
From an authorization perspective, we can immediately see how this approach is way too static. Let’s take Role-Based Access Control (RBAC), for example. In RBAC, we take the role of a user and determine if they can perform a certain operation or action. RBAC doesn't stop with roles - even an authorization model as simple as RBAC contains more components, such as the Action a user is attempting to perform and the Resource they are performing it on.
The JWT only contains the first part of these three components—the user and potentially the role they have assigned. If user roles are passed as part of the JWT, it is impossible to create dynamic roles, rendering our authorization layer completely static. This means outdated permissions are being honored until the token expires.
If, for example, I have an admin user that I want to downgrade to a standard user, I cannot do that unless the JWT is revoked. Because JWTs are verifiable by principle, there’s no way to revoke it before the token expires. Usually, JWTs nowadays are very short-lived, so with access tokens, the TTL (time to live) could be around 60 seconds, and the SDK refreshes them for you. Hence, we can’t solely rely on the roles that are on the JWT.
JWTs are not Fine-Grained
As JWTs only contain information about individual users, there are very hard limits on the amount of data you can (and should) store in them. For models like Attribute-Based Access Control (ABAC) and Relationship-Based Access Control (ReBAC), using JWTs to create authorization queries directly becomes impossible.
JWTs have a size limit. This means that if we want to grant a user access to 1000 files, we’ll have to create a 20,057-character JWT—and that’s considering a simple file name. With the full path name, that’s going to be even longer.
The first step in designing an authorization layer is determining the resources to which you need to manage access. Each type of resource and its place in the overall structure of your application might require you to utilize a different type of authorization model (RBAC, ABAC, ReBAC, or a combination of them). Using JWTs might be a quick temporary solution, but it is far from sustainable.
It is important that you familiarize yourself with the available policy models out there, their pros and cons, and try to assess which ones are the best fit for your application
Authorization is a Complex Issue
It’s also important to mention the challenge of building proper authorization is much more complex than using a simple token. As a baseline, authorization requires centralized management, auditability, and its policies to be flexible. These are all things JWTs are not meant or able to provide. Building a secure, scalable authorization layer for your application is a formidable challenge - one you should prepare for thoroughly.
In a nutshell, it is crucial to follow authorization best practices such as decoupling policy and code, making sure your authorization is event-driven, providing relevant back-offices for stakeholders, enabling customers-facing interfaces, and utilizing GitOps. You can read more about these here.
What Role Should JWTs Play in Authorization?
JWTs are not made for authorization—they are made for verification, exchanging tokens, and token-based authentication. If we use JWTs as they were meant to be used—as a source of truth for user identity—we can utilize them to query an external authorization service about what a user can or cannot do. But, this use should not include any of the authorization logic as part of the JWT itself.
Conclusion
JWTs are a powerful authentication component that can significantly enhance the security and efficiency of web applications. However, they are not designed for authorization, and using them as such can lead to security vulnerabilities and management challenges. By understanding the proper use of JWTs and adhering to best practices, developers can create more secure and maintainable applications. Use JWTs to verify identities, not to manage permissions. Rethink your authorization strategies and leverage specialized tools and services to handle the dynamic nature of authorization.
Written by
Daniel Bass
Application authorization enthusiast with years of experience as a customer engineer, technical writing, and open-source community advocacy. Comunity Manager, Dev. Convention Extrovert and Meme Enthusiast.