DEV Community

Mustafa ERBAY
Mustafa ERBAY

Posted on • Originally published at mustafaerbay.com.tr

JWT Storage: LocalStorage or HttpOnly Cookie?

JWT Storage: LocalStorage or HttpOnly Cookie?

The use of JSON Web Tokens (JWT) is quite common in web applications for user authentication and authorization processes. However, securely storing these tokens on the client-side (browser) is a critical issue that should not be overlooked. There are generally two main options we encounter: the browser's localStorage or HttpOnly cookies. Let's take a deep dive into the advantages, disadvantages, and scenarios where we should prefer one over the other.

In this comparison, I will cover many aspects of both storage mechanisms, from security to performance, ease of use to scalability. My aim is to help you make the right decision for your own projects. Remember, choosing a technology always involves a trade-off, and the best solution is shaped by the specific needs of your project.

The Nuances and Risks of LocalStorage

localStorage is a key-value based storage mechanism offered by the browser. Data persists even if the browser is closed and is easily accessible via JavaScript. This simplicity might seem appealing for storing tokens like JWTs. However, this ease comes with significant security vulnerabilities.

The biggest risk is that localStorage is readable by JavaScript. This means any JavaScript code running on your web page (your own code, third-party libraries, or worst of all, malicious code injected as a result of a Cross-Site Scripting (XSS) attack) can access your tokens. When an XSS vulnerability is detected, attackers can steal a user's session and act on their behalf. For example, when you unknowingly execute malicious script while commenting on a blog site or sending a message on a forum, your JWT in localStorage can be directly compromised.

⚠️ XSS Attack Scenario

Let's assume there's a vulnerability in the input validation during the "add to cart" functionality on an e-commerce site. An attacker could exploit this vulnerability to inject a command like "><script>alert(document.cookie)</script>. If the JWT is stored in localStorage and this script is executed, it becomes quite easy for the attacker to steal the token from localStorage. This situation can lead to the hijacking of the user's session.

Furthermore, localStorage's storage capacity is typically around 5MB, which might not be enough for large tokens or additional data. From a performance perspective, reading and sending data from localStorage on every request can create an additional overhead on the browser's JavaScript engine. Therefore, when using localStorage for JWT storage, potential security risks and performance impacts must be carefully evaluated.

A Real-Life Case: XSS and Token Theft Experience

A few years ago, on a client project, we were working on a web application where users created interactive reports. The application's frontend was written with Vue.js and used JWT authentication. Tokens were stored in localStorage. Towards the end of the development process, a security tester found an XSS vulnerability in an input field within a dynamically generated chart component. The attacker, by injecting specially crafted script into this field, could control the JavaScript running on the page.

The tester, using this vulnerability, executed the command localStorage.getItem('authToken') and successfully hijacked the user's session with the obtained token. This situation required immediate intervention. We quickly released a hotfix, disabled the script execution feature in the relevant input field, and asked all users to refresh their tokens. After this incident, we decided to migrate the project's JWT storage strategy to HttpOnly cookies. This experience painfully demonstrated how vulnerable localStorage is to XSS attacks.

Security Advantages of HttpOnly Cookies

Cookies with the HttpOnly flag are automatically sent to the server by the browser and cannot be accessed by JavaScript. This, unlike localStorage, significantly prevents XSS attacks from stealing tokens. This is because malicious scripts cannot access HttpOnly cookies via document.cookie. This feature makes HttpOnly cookies a more secure option for storing sensitive information like JWTs.

An HttpOnly cookie is sent to the browser with a Set-Cookie HTTP header. For example, when your server-side application (e.g., using Express with Node.js) generates a JWT, it can send this token to the browser with a header like this:

Set-Cookie: authToken=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...; HttpOnly; Secure; SameSite=Strict; Path=/
Enter fullscreen mode Exit fullscreen mode

The HttpOnly flag here prevents JavaScript from accessing this cookie. The Secure flag ensures the cookie is only sent over HTTPS, providing an additional layer of protection against man-in-the-middle (MITM) attacks. SameSite=Strict ensures the cookie is only sent with same-site requests, reducing the impact of CSRF (Cross-Site Request Forgery) attacks.

💡 The Role of HttpOnly Cookies

HttpOnly cookies have become standard for session management and authentication tokens. Browsers automatically send these cookies with requests to the relevant domain. This eliminates the burden of developers manually adding the token to every request and simplifies the authentication process on the server side. Furthermore, this automatic sending feature prevents tokens from appearing in client-side code, providing an additional layer of security.

However, HttpOnly cookies also have their own challenges. Since browsers send these cookies automatically, when you want to read the token on the client-side and perform operations on it (e.g., taking the token from the cookie and adding it back to the header when preparing an API request, instead of adding it to the header), you won't have direct access. This situation might require you to make certain architectural decisions, especially when developing SPAs (Single Page Applications).

CSRF Protection with HttpOnly Cookies

Another important security feature of HttpOnly cookies is the protection they offer against Cross-Site Request Forgery (CSRF) attacks when used with the SameSite attribute. CSRF attacks aim not to steal the user's session, but to use the user's browser to perform actions the user has not approved or is unaware of.

For example, while a user is logged into a banking site, a malicious website might secretly send a payment order to the banking site without the user's knowledge. If the session information is stored in HttpOnly cookies and these cookies are set to SameSite=Strict, the browser will not send the cookie for this request coming from a different site. This leads to the CSRF attack failing.

Set-Cookie: sessionId=abc123xyz; HttpOnly; Secure; SameSite=Strict
Enter fullscreen mode Exit fullscreen mode

In the example above, the SameSite=Strict setting enforces that the cookie is only sent for requests to the same site (i.e., requests originating from bank.com). If a user is on malicious.com and a POST request is made to bank.com, the browser will not include the sessionId cookie in this request. This is a strong defense mechanism against CSRF attacks.

ℹ️ The Importance of the SameSite Attribute

The SameSite attribute can take three different values:

  • Strict: The browser sends the cookie only for same-site requests. This is the most secure option but can cause issues in some cross-site scenarios (e.g., arriving from another site via a direct link).
  • Lax: The browser sends the cookie with same-site requests for top-level navigation (GET, HEAD, OPTIONS, TRACE). This is more flexible than Strict and prevents most CSRF attacks while preserving basic navigation functionality. This is the default value.
  • None: The browser sends the cookie with all requests. When using this option, the Secure attribute must also be specified.

However, the SameSite=Strict setting can negatively affect user experience in some situations. For example, if a user clicks a link from one site (e.g., siteA.com) to another site (siteB.com) and needs to log in to siteB.com, the Strict mode might block this request because the request originates from a different "site". In such scenarios, SameSite=Lax might be a more suitable option.

Architectural Approaches to Managing JWTs

When using HttpOnly cookies, how we send the JWT to the server is an important architectural consideration. Traditionally, authentication tokens are sent with the Authorization: Bearer <token> header. However, since HttpOnly cookies are inaccessible to JavaScript, we cannot apply this approach directly.

In this case, two common strategies can be followed:

  1. Server-Side Rendering (SSR) or Hybrid Approaches: If your application is rendered on the server-side (e.g., with frameworks like Astro, Next.js, Nuxt.js), the server can read the token from HttpOnly cookies upon receiving a request and use it directly in API requests. In this scenario, client-side JavaScript does not need to access the token.

  2. API Gateway or Backend for Frontend (BFF): By using a separate BFF layer, you can receive client requests, read the token from HttpOnly cookies, make requests to the actual service APIs with this token, and return the results to the client. This eliminates the complexity of token management on the client side.

ℹ️ Using HttpOnly Cookies with Astro

Modern frontend frameworks like Astro, with their SSR capabilities, simplify JWT management with HttpOnly cookies. Within an API route or a server-side component in Astro, you can access the token in a cookie using a method like request.cookies.get('authToken'). You can then use this token in the headers object when sending it to another service via fetch. This reduces the direct interaction of client-side JavaScript with the token.

If your application runs entirely on the client-side and you are using HttpOnly cookies, you might need to use an intermediary layer on the server side (e.g., an Nginx proxy_set_header directive or a Node.js server) to add the token to a header. This intermediary layer will take the token from the incoming request's HttpOnly cookie and add it to the Authorization header of the request sent to the target API. This approach simplifies client-side code and reduces security risks.

Comparison of Storage Options

The following table summarizes the key features of localStorage and HttpOnly cookies:

Feature LocalStorage HttpOnly Cookie
Access Method JavaScript Browser (with automatic requests)
XSS Protection None (Easily stolen) Yes (JavaScript access is blocked)
CSRF Protection None (Requires manual implementation) Yes (With SameSite attribute)
Storage Capacity Typically 5MB Varies by browser and site (generally more)
Session Duration Persistent until cleared by the browser Determined by Expires or Max-Age
Automatic Sending No (Must be manually added to request header) Yes (To all requests to the relevant domain)
Ease of Use High (Simple access via JavaScript) Medium (Intermediary layer or SSR may be required)
Security Low High

Looking at this table, we can clearly say that HttpOnly cookies are a significantly more secure option than localStorage for storing sensitive information like JWTs. The ease of localStorage requires serious compromises in terms of security.

Performance and Scope Analysis

From a performance perspective, reading and writing data from localStorage consumes CPU cycles on the JavaScript engine. Retrieving the token and adding it to the HTTP header on every request creates a small performance cost, especially in applications that make frequent API calls. On the other hand, HttpOnly cookies are managed automatically by the browser and sent as part of the HTTP request. This reduces the workload on client-side JavaScript.

However, HttpOnly cookies can also have some potential performance impacts. Especially if there are large tokens or a large number of cookies, the amount of data sent with each request increases. This can affect network bandwidth and server-side processing time. Nevertheless, these impacts are generally negligible when compared to the potential cost of XSS attacks.

In terms of scope, localStorage operates on a browser basis, and cookies are similarly tied to the browser and domain. However, with additional attributes like Secure and SameSite for HttpOnly cookies, it's possible to implement more granular control and advanced security policies. This makes HttpOnly cookies a more comprehensive security solution.

Ongoing Issues and Solution Recommendations

While HttpOnly cookies are a more secure option than localStorage, they are not perfect. In particular, managing HttpOnly cookies can introduce some additional complexities for developers. For example, the inability of client-side JavaScript to access the token can create difficulties in some modern frontend architectures.

As a solution, the BFF (Backend for Frontend) pattern is quite effective. The BFF layer receives client requests, retrieves the token from HttpOnly cookies, makes the necessary service calls, and returns the results to the client. This way, the client side only communicates with the BFF, and token management is handled entirely on the server side. This approach is a great way to simplify the client side, especially in complex microservice architectures.

Another potential issue is the synchronization of cookies across browsers or managing sessions on different devices. If a user accesses the application from different devices and you want their sessions to be synchronized, HttpOnly cookies alone might not be sufficient. In such cases, using HttpOnly cookies in conjunction with a centralized session management mechanism on the server side might be more sensible.

🔥 Storing All JWTs in One Place Can Be Risky

No storage method is 100% secure. Since JWTs contain sensitive information, it's important not only to store the token but also to secure the token itself. This includes measures like using strong encryption algorithms, keeping token expiration times short, and implementing mechanisms to invalidate tokens when necessary (e.g., a token blacklist). Processing JWTs securely on the server side, not just on the client side, is of great importance.

Furthermore, browser cookie policies can change over time, leading to unexpected behavior. Therefore, it's important to follow current browser documentation and ensure that attributes like HttpOnly, Secure, and SameSite are implemented correctly.

Conclusion: A Security-First Approach

So, localStorage or HttpOnly cookie? Based on my experience and the analysis presented here, HttpOnly cookies should definitely be preferred for storing sensitive authentication tokens like JWTs. The easy accessibility of localStorage via JavaScript makes it excessively vulnerable to XSS attacks. An XSS vulnerability can directly lead to session hijacking by stealing the token from localStorage.

HttpOnly cookies eliminate this risk by being automatically managed by the browser and hidden from JavaScript. When combined with the SameSite attribute, they also provide strong protection against CSRF attacks. The minor additional complexity they introduce during development is quite insignificant compared to the security advantages they offer.

If you are developing a modern SPA and find it challenging to work with HttpOnly cookies, you can overcome these difficulties by adopting approaches like a BFF layer or server-side rendering (SSR). Remember, web security is a continuous battle, and every correct architectural decision we make can make a big difference in protecting our users' data.

The next step is to integrate HttpOnly cookies into your application to securely store your JWTs. This will ensure that both you and your users are safer.

Top comments (0)