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.
To keep them secure, you should always store JWTs inside an HttpOnly cookie. This is a special kind of cookie that’s only sent in HTTP requests to the server. It’s never accessible (both for reading and writing) from JavaScript running in the browser.
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.
The site contains a list of the most popular libraries that implement JWT, including libraries for Node.js, Python, Rust, Go, JavaScript, and many more.
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
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Actix Web is definitely a compelling option to consider, whether you are starting a new project or considering a framework switch.
Explore the integration of Tailwind CSS with React Native through NativeWind for responsive mobile design.
The react-tv-space-navigation library offers a comprehensive solution for developing a cross-platform TV app with React Native.
Explore some of the best tools in the React ecosystem for creating dynamic panel layouts, including react-resizable-layout and react-resizable-panels.
21 Replies to "JWT authentication: Best practices and when to use it"
I don’t understand some of the claims here.
“Don’t store it in local storage (or session storage). If any of the third-party scripts you include in your page gets compromised, it can access all your users’ tokens.”
Why does this matter, when you protect against CSRF with CSRF tokens?
I tried storing it in cookie httpOnly but my problem is I cannot pass as request authorization header when making a request to the backend. How will this be solved?
You don’t. Cookies are send with every request you make to the server, so you read from the cookie in the backend instead of the authorization header.
I am thinking to store in authorization the id from db that contains the token, in authorization header the id will be used, or just encrypt all tokens with your master password then add in header, then decrypt at some point :D, really nothing seems safe
How to send cookie while making api call. I tried to add cookie in the header but no use
While testing, superagent makes it easy to set token in cookies. I guess any http agent will help too
did you solve the problem coz i also had the same problem
After some research, yes. It’s automatically passed into the request cookies. Before I use req.headers.authorization in my middleware, now I have to use `req.cookies[‘name’]`. The idea of setting cookie as httpOnly is that you can never call it using JS to alter like localstorage.
×´ there is always the possibility that it’s intercepted and the data deciphered×´ – deciphered is not the right word here since JWT are serialised, not encrypted
“the possibility that it’s intercepted and the data deciphered, exposing your user’s data.”
We only store enough information to identify the user in the jwt token. It can be the user’s id, email, or even another access token (in case you want to implement remote logout or similar features). We don’t store sensitive data (e.g. password,…) in the token, so this should not be an issue.
“Using JWT to authorize operations across servers” do you have any examples for this?
As http is stateless, every request made is new to server, to solve this or remember user/request, people use sessions, where server sends session id, like php sends PHPSESSID(key of cookie) stored at client side in cookie. When user makes another request php server calculate that it’s not new user. Now what if your server redirects you to the another physical server let’s say from example.net to cdn.example.org having different task assigned to them. This can cause problem because only one of the server has the power/logic to decipher that sessid right? Now that can be solved with jwt since you need only need to copy secret_key or simply .env file. And you can still verify and compare passwords
I am new to JWT.
If not through JWT how should we send sensitive data (like a password) to a server while logging in.
Actually it is. If the backend gets id=1 as part of the JWT payload, then it will assume the request is made from the correctly authenticated user with id=1, and thus will complete any request made.
Sending a password (either for logging in, or for creating an account) has nothing to do with JWT. JWT is about authenticating to the server after you have already sent the password. To do this correctly you must only connect via HTTPS.
Hi there, nice article.
I still have a question: if JWT is stored in cookies (secured & httpOnly), then the application is vulnerable to CSRF attacks, am I right?
And if the JWT is stored elsewhere accessible from JS then, the app is vulnerable to XSS.
What is the best solution?
Good article. Thank you
Great article. Really helped me figure out my backend authentication strategy, thanks again LogRocket!
Recommendation: replace the terms “blacklist” and “whitelist” with “blockedlist” and “allowedlist”. I know they’re traditional terms but the racial undertones are not friendly and could be done away with 🙂
There are so many issues with this article. Let’s start with the basic out of the gate. JWT is a token format. There are two common implementation uses of JWT, JWS and JWE. JWS is a signed token, JWE is an encrypted token. Use the correct JWS/JWE for what you are trying to protect.
Next, JWT is a text string, this can be embedded in email as part of a link to not expose information, it can be placed in cookies, it can be placed HTTP headers…. How JWT is used has nothing to do with the specification.
Next consider the fact that OAuth utilizes JWT, and this is the foundational protocol for the majority of single sign on (SSO) applications out there. If JWT was this bad, you would think one of these companies would point it out.
If you use cookies to store the authentication data, you are susceptible to CRSF. In fact CSRF is pretty much only possible if you store the authentication data as a cookie. On the flip side, store data in local or session makes it XSS possible.
If you want to ensure your system is secure, you really need to come at the problem from multiple directions. For web applications, using both a cookie and token in the HTTP Header with different values will provide the best protection against XSS and CSRF
Tim
Less secure? Than what?
Storing *anything* in a cookie ? Bad reccomendation (csrf).
Xss ? I dont see the relevance of that in this article whatsoever. Using secure http-only cookies doesn’t help against xss.
is no one having issues with incognito mode? My auth flow won’t work on Chrome’s incognito mode. The cookie were the JWT is stored is completely blocked by chrome. I don’t know what todo. It’s considered third party because I set the cookie from the ‘backend’ which is hosted in a different url than the frontend. this is how i set the cookie res.cookie(‘token’, token, {
httpOnly: true,
secure: true,
sameSite: ‘none’,
partitioned: true, // I added this due to Chrome’s warning that they will block all third party cookies some time soon
});