Best Practices for Securing JWT Tokens: A Comprehensive Guide

JWTs enable stateless authentication, but securing them is critical. Avoid storing tokens in local storage due to XSS risks. Instead, use HttpOnly, Secure cookies to prevent access via JavaScript and limit CSRF attacks. Pair short-lived access tokens with refresh tokens for enhanced security.

Best Practices for Securing JWT Tokens: A Comprehensive Guide

JWT (JSON Web Tokens) has become a widely adopted solution for transferring claims between two parties, typically a front-end and a back-end, enabling stateless authentication in web applications. Their simplicity, ease of integration, and flexibility make JWTs an attractive option for session management, but with great power comes great responsibility. Securing JWT tokens effectively is crucial to protect your applications from various attacks. In this guide, we’ll explore best practices for securing JWT tokens and how to implement them.

What are JWT Tokens?

JWT is a compact, URL-safe means of representing claims between two parties. It consists of three parts: a header, payload, and signature. JWTs are used primarily to establish stateless sessions between the front end and the back end, allowing a server to verify the identity of the client without maintaining session data on the server.

JWT.IO
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties.

After the user is authenticated, a JWT is sent to the client sidemethod has its, where it must be securely stored for future authenticated requests. However, improper storage and handling of JWTs can expose applications to severe vulnerabilities, such as cross-site scripting (XSS) and cross-site request forgery (CSRF) attacks. Therefore, securing JWT tokens is critical.

Storage Options for JWT Tokens

There are four common ways to store JWT tokens on the client side:

  • Local Storage
  • Session Storage
  • Cookies
  • In-memory

Each method has its pros and cons, which we'll explore in detail.

1. Local Storage

Pros: Local Storage persists across browser sessions, meaning that even if a user closes the browser, the JWT remains stored. It offers isolation per the Same-Origin Policy, ensuring that another cannot access data stored by one origin.

Cons: Local Storage is vulnerable to XSS attacks. If an attacker injects malicious JavaScript code into your site, they can easily retrieve sensitive data, such as JWTs, with a single line of code:

localStorage.getItem("token");

Additionally, attackers can iterate over all stored items:

for (let key in localStorage) {
   console.log(localStorage.getItem(key));
}

Recommendation: Avoid storing JWTs in Local Storage, especially for sensitive tokens like access tokens.

2. Session Storage

Pros: Session Storage also adheres to the Same-Origin Policy but clears data when the browser session ends. Data stored in Session Storage is isolated between browser tabs, which reduces the attack surface for tab-napping attacks.

Cons: Like Local Storage, Session Storage is vulnerable to XSS attacks, making it unsuitable for securely storing JWT tokens:

sessionStorage.getItem("token");

Recommendation: While Session Storage is more secure than Local Storage in some cases, avoid using it for sensitive JWTs unless you are confident in your XSS protections.

3. Cookies

Cookies are a popular choice for storing JWT tokens. They have specific flags that improve security:

HttpOnly Flag

  • HttpOnly cookies cannot be accessed by JavaScript, which protects them from XSS attacks. If you set the HttpOnly flag to true, the cookie is only accessible via HTTP requests.
  • Pros: It protects the token from XSS attacks since JavaScript cannot retrieve it.
  • Cons: While HttpOnly cookies protect token confidentiality, they are still vulnerable to CSRF attacks. Attackers can exploit CSRF vulnerabilities to send unauthorized requests using a valid JWT without the user's knowledge.

HttpOnly: false

Cookies with HttpOnly: false can be accessed via JavaScript, making them vulnerable to XSS attacks. Here's how an attacker could retrieve such a token:

document.cookie.split('; ').find(
   row => row.startsWith('token')
).split('=')[1];

Recommendation: Always use HttpOnly cookies for storing JWTs and mitigate CSRF attacks by pairing this strategy with additional protection like CSRF tokens or SameSite flags.

SameSite Flag

The SameSite flag helps prevent CSRF by ensuring cookies are only sent with requests originating from the same site. This reduces the risk of third-party sites using the JWT in malicious requests.

Secure Flag

The Secure flag ensures cookies are only sent over HTTPS, preventing them from being exposed via unsecured HTTP connections.

Best Practice for Cookies:

  • HttpOnly: true
  • Secure: true (use HTTPS)
  • SameSite: lax or strict (depending on your application's needs)

4. In-memory Storage

Pros: Storing JWTs in memory is the most secure option in terms of exposure to attacks like XSS, as tokens are not persisted in the browser. Once the page is refreshed or the session ends, the token is gone.

Cons: Since tokens are stored in memory, they are lost if the user refreshes the page or navigates away from the site, which might impact the user experience.

Web Workers for Enhanced Security

A more secure in-memory storage solution involves using Web Workers. Web Workers run in a background thread and communicate with the main thread via MessageChannels. Secrets stored in a Web Worker are not directly accessible to the browser’s main JavaScript context, mitigating XSS risks.

However, like HttpOnly cookies, an attacker could use XSS to send messages to the Web Worker, indirectly using the secret.

Recommendation: When possible, store JWT tokens in Web Workers to isolate secrets from the main application and minimize XSS risks.

Implementing a Secure JWT Flow

To maximize security, it’s recommended to use two tokens:

  • Access Token: Short-lived token for accessing resources.
  • Refresh Token: Long-lived token used to renew the access token when it expires.
res.cookie("accessToken", accessToken, {
  secure: true,
  httpOnly: true,
  sameSite: "none",
  priority: "high",
});

res.cookie("refreshToken", refreshToken, {
  secure: true,
  httpOnly: true,
  sameSite: "none",
  priority: "high",
});

Best Practices:

  1. Store JWT in HttpOnly Cookies: The access token should be stored in a secure, HttpOnly cookie. This ensures that the token cannot be accessed via JavaScript, protecting it from XSS attacks.
  2. Use Refresh Tokens: Implement a short-lived access token and a long-lived refresh token. When the access token expires, the refresh token should be used to request a new one. This minimizes the window of opportunity for attackers to exploit compromised tokens.
  3. Short Expiry Times: Tokens should have short expiry times to limit the damage if they are stolen. After expiry, use the refresh token to obtain a new access token.
  4. Use HTTPS: Always transmit tokens over HTTPS to prevent them from being exposed to man-in-the-middle (MITM) attacks.
  5. CSRF Mitigation: Along with HttpOnly cookies, implement additional measures to mitigate CSRF attacks, such as CSRF tokens or the SameSite attribute in cookies.

Securing JWT tokens is a vital part of ensuring your web application’s integrity and protecting user data. By choosing the right storage mechanism and following best practices like using HttpOnly cookies, refresh tokens, and implementing short expiry times, you can significantly reduce the risks posed by XSS and CSRF attacks.

Remember, security is a moving target, and regularly revisiting your JWT storage strategies is essential as threats evolve.

That's it for today! See ya 👋


References