Editor’s note: This JWT authentication tutorial was last updated on 5 December 2023 to discuss some of the inefficiencies of JWT authentication, including token invalidation and size constraints.
Ensuring the security of user data and establishing secure communication between servers and clients is a critical aspect of web development. Various tools and packages have been developed to facilitate this. Among them, JSON Web Tokens (JWTs) have emerged as a popular choice over the years.
However, as with any technology, JWTs have sparked controversy. Some caution against their use, while others view them as an excellent authentication method.
In this article, we will explore JWT authentication, its limitations, and scenarios where its implementation proves invaluable.
JSON Web Tokens (JWTs) are a standardized way to securely send data between two parties. They contain information (claims) encoded in the JSON format. These claims help share specific details between the parties involved.
At its core, a JWT is a mechanism for verifying the authenticity of some JSON data. This is possible because each JWT is signed using cryptography to guarantee that its contents have not been tampered with during transmission or storage.
It’s important to note that a JWT guarantees data ownership but not encryption. The reason is that the JWT can be seen by anyone who intercepts the token because it’s serialized, not encrypted.
It is strongly advised to use JWTs with HTTPS, a practice that extends to general web security. HTTPS not only safeguards the confidentiality of JWT contents during transmission but also provides a broader layer of protection for data in transit.
To understand the problem JWT aims to solve, let’s look at how we traditionally handle authentication and authorization.
During the login process, users log in with their credentials. The server authenticates the user, often by checking the entered credentials against a database.
Upon successful authentication, a unique session identifier is generated and sent back to the client. This session ID is then stored on the user’s device.
For each subsequent user request, the session ID is sent to the server either in a cookie or as a header. The server looks up the session ID in its database to identify the user and determine their authorization level.
The problem with this approach is that for every request, the server takes a round trip to the database. This process often slows down the application. Here’s where JWTs come in.
JWT, especially when used as a session, attempts to eliminate the subsequent database lookup.
Like before, users would log in with their credentials. The server authenticates the user, often by checking the entered credentials against a database. Upon successful login, the server creates a JWT containing user information and a signature to verify its authenticity.
The server sends the JWT to the client. Then, each subsequent request from the client includes the JWT. The server validates the token’s signature to ensure it hasn’t been tampered with. The user’s identity and authorization details are extracted from the token, eliminating the need for constant database lookups.
Using JWTs for session tokens might seem like a good idea at first because it is cryptographically signed and stores user details, thus eliminating database lookup. These might seem like valid reasons, but there are quite a few downsides to using JWT as a session mechanism.
Here’s a non-exhaustive list of problems associated with using JWT as a session mechanism.
In many complex real-world apps, you may need to store a ton of different information. When used with cookies, this either adds up to a lot of overhead per request or exceeds the allowed storage space for cookies, which is 4KB. This often leads people to store the JWTs in localStorage instead.
Storing sensitive data in localStorage comes with many problems on its own. For a little context, If you store it inside localStorage, the data accessible by any script inside your page. This is as bad as it sounds: an XSS attack could give an external attacker access to the token.
Invalidating a single token in JWT can also be a challenge. This is because they are self-contained and do not have a central authority for invalidation, unlike traditional sessions.
This issue is significant — for instance, when dealing with bad actors, suspending their account won’t immediately revoke their access because JWTs persist until expiration.
One proposed solution is to change the server secret key, which would invalidate all JWTs. However, this approach can be inconvenient for users because it would cause their tokens to expire without reason, logging everyone out.
Another suggestion is to keep a list of invalidated tokens in the database. However, doing so would undermine the built-in expiration functionality of JWTs, and checking every request against this list would contradict the efficiency offered by JWTs.
Also, while the security risks are minimized by sending JWTs using HTTPS, there is always the possibility that it’s intercepted and the data deciphered, exposing your user’s data. Remember, JWTs can be seen by anyone who intercepts the token because it’s serialized, not encrypted.
Additionally, JWT has a wide range of features and a large scope, which increases the potential for mistakes. Many libraries that implement JWT have had security issues over the years because the JWT spec itself had security issues.
Check out this video for a more in-depth explanation of the security issues with using JWT.
Despite the risk they pose, it is still important to understand when to opt for JWTs rather than using cookies. Despite their utility, JWTs are often misused. The challenge primarily lies in knowledge gaps. As a rule of thumb, JWTs are most beneficial when:
A very common use for JWT — and perhaps the only good one — is as an API authentication mechanism. JWT technology is so popular and widely used that Google uses it to let you authenticate to its APIs.
The idea is simple: you get a secret token from the service when you set up the API:
On the client side, you create the token (there are many libraries for this) using the secret token to sign it.
When you pass it as part of the API request, the server will know it’s that specific client because the request is signed with its unique identifier:
A JWT needs to be stored in a safe place inside the user’s browser. We already established that storing sensitive data inside localStorage is a bad idea. To reiterate, whatever you do, don’t store a JWT in localStorage (or sessionStorage). If any of the third-party scripts you include in your page are compromised, it can access all your users’ tokens.
JWTs are well-suited for server-to-server or microservice-to-microservice communication scenarios within a backend architecture. In this context, JWTs serve as a means of securely transmitting information between services for authorization and authentication purposes.
Say you have one server where you are logged in, SERVER1, which redirects you to another server, SERVER2, to perform some kind of operation.
SERVER1 can issue you a JWT that authorizes you to SERVER2. Those two servers don’t need to share a session or anything to authenticate you. The token is perfect for this use case.
Refreshing expired tokens with JWT typically involves the use of a refresh token mechanism. Refresh tokens are long-lived tokens that can be used to obtain a new JWT when the original token expires.
Here’s a typical flow for refreshing expired tokens:
Users would log in with their credentials. The server generates both an access token (JWT) and a refresh token. The access token has a relatively short expiration time while the refresh token has a longer expiration time.
Then, the server sends the JWT and Refresh token to the client. The refresh token is usually stored in a secure cookie. Each subsequent request from the client includes the JWT.
The user’s identity and authorization details are then extracted from the token, eliminating the need for constant database lookups.
When the access token expires, the client sends a request to a token refresh endpoint with the refresh token. The server validates the refresh token. If the refresh token is valid, the server issues a new access token and, optionally, a new refresh token.
A common solution to address the non-revocability issue of JWTs is to maintain a database of “revoked tokens” and check it for every request. If the token is found to be revoked, the user is denied access to the desired resource.
It’s worth noting, however, that this approach adds an extra database call to check if the token is revoked, which defeats the purpose of using JWT in the first place.
How do you decide which JWT library to use in your project? A good place to start is this list of JWT libraries for token signing and verification.
Select your language of choice and pick the library that you prefer — ideally, the one with the highest number of green checks:
JWT is a very popular standard you can use to trust requests by using signatures and exchange information between parties. Make sure you know when it’s best used, when it’s best to use something else, and how to prevent the most basic security issues.
Install LogRocket via npm or script tag.
LogRocket.init() must be called client-side, not
We explore the fusion of TensorFlow and Rust, delving into how we can integrate these two technologies to build and train a neural network.
SignalDB enables automatic data synchronization between your components and a local in-memory or persistent database.
Understanding how layouts, nested layouts, and custom layouts work in Next.js is crucial for building complex, user-friendly projects.
Extract YouTube video data using OpenAI and LangChain for quick, cost-effective insights without watching the videos.