JWT storage 101: How to keep your tokens secure
Want to keep your JWTs safe from attackers? This guide covers the best practices for securely storing your tokens and ensuring your app's security.
In the world of web development and authentication, JSON Web Tokens (JWTs) have become one of the most popular methods for securely transmitting information between parties. These tokens are compact, URL-safe, and allow for efficient, stateless, secure communication between users and servers, making them a staple in modern app development.
However, like any powerful tool, JWTs come with their own set of security challenges—especially when it comes to storing them properly. If not handled correctly, these tokens can become a prime target for attackers, potentially exposing your users' sensitive data.
In this article, we’ll explore the best practices for storing JWTs securely, helping you protect your app from common vulnerabilities, and ensuring that your tokens stay out of the wrong hands. We will focus on how to store JWT securely on the client since this is where we have most problems due to the fact that browsers and mobile apps cannot safely hold secrets.
Let’s dive into securing your JWTs!
What is a JWT?
A JWT is a compact, URL-safe token that contains a set of claims. It typically consists of three parts:
- Header: Contains metadata about the token, like its type and the signing algorithm.
- Payload: Contains the claims or the actual data, such as user ID or roles.
- Signature: The signature ensures the integrity of the token. It’s created by signing the header and payload using a secret or a private key.
JWTs are primarily used for authentication and authorization purposes. When a user logs in, the server generates and signs a JWT containing user information. The token is sent to the client, which can then include it in requests to the server to authenticate future requests.
Since JWTs often contain sensitive data, ensuring their security is paramount. If an attacker gains access to a JWT, they may be able to impersonate a user or gain unauthorized access to a system. The security of JWTs relies heavily on how and where they are stored on the client side. Improper storage can expose these tokens to theft and misuse.
Storing JWT in HTTP cookies (recommended)
Cookies are small pieces of data stored by the browser and sent with every request to the domain they belong to. They can be configured with additional security flags to help protect them:
- The
HttpOnly
flag makes cookies inaccessible to JavaScript, reducing the risk of XSS attacks. - The
Secure
flag ensures that cookies are only sent over HTTPS. - The
SameSite
flag is a security feature used in web browsers to control when cookies are sent along with cross-site requests. It helps protect against cross-site request forgery (CSRF) attacks, ensuring that cookies are only sent in a secure context, such as same-site requests. The SameSite attribute can have three possible values:SameSite=Strict
: This means the cookie will only be sent if the request is from the same origin (domain) as the site setting the cookie. It won't be sent for cross-site requests, even if the user is navigating from one site to another. This provides the highest level of security.SameSite=Lax
: This is a more lenient setting. The cookie will be sent with same-site requests, and it will also be sent with cross-site requests only for top-level navigation (such as clicking a link). However, it will not be sent with embedded content (like images or frames) from another site. This strikes a balance between security and usability.SameSite=None
: This allows the cookie to be sent with both same-site and cross-site requests, meaning it can be used for things like third-party services (e.g., embedded ads, cross-site authentication). However, when usingSameSite=None
, the cookie must also have the Secure flag set, meaning it can only be sent over HTTPS connections.
- The
MaxAge
orExpires
attribute ensures the JWT has a limited lifespan, requiring re-authentication after a certain period.
When the user logs in and the server issues a JWT, the token is set as an HTTP cookie:
Some things to keep in mind when you store JWT in cookies are:
- Cookies have a size limit (usually 4 KB), so ensure the token is small enough to fit within this limit.
- Use short expiration times for the token and refresh it when necessary to balance usability and security.
- If not configured properly, cookies are vulnerable to cross-site request forgery (CSRF) attacks. However, this risk can be mitigated with the use of the SameSite cookie attribute.
Storing JWT in local storage
Local storage is a key-value store that allows data to persist even after the browser is closed. It is easy to use and accessible across page reloads.
When the user logs in and the server provides a JWT, store it in the browser's local storage:
The token can be retrieved later when making API requests and then added manually to each API request in the Authorization
header:
localStorage
allows you to store a larger amount of data (usually around 5-10 MB, depending on the browser), compared to cookies (typically around 4 KB). This can be useful if your JWT contains many claims or other data.
While localStorage provides ease of access, it also presents significant security risks:
localStorage
is vulnerable to cross-site scripting (XSS) attacks. If an attacker can inject malicious JavaScript into your web page (through a vulnerability such as unsanitized user input), they could retrieve the JWT from local storage and use it to impersonate the user.- JWTs often have an
exp
(expiration) claim, but local storage does not support automatic expiration of items. The token will remain in the storage until explicitly removed by the application or the user, potentially leaving old or invalid tokens in storage. This creates a scenario where an attacker who gains access to the token can use it even after it should have expired, unless proper expiration checks are implemented in the application.
localStorage
is not related to network traffic, so it doesn't have a direct protection mechanism like cookies with theSecure
flag (which ensures data is only transmitted over HTTPS). WhilelocalStorage
itself isn’t sent in network requests, if the application isn't protected by HTTPS, an attacker could still intercept the network traffic and steal the JWT if it is sent in request headers.- Unlike cookies with
SameSite
attributes,localStorage
does not have any built-in protections against cross-site request forgery (CSRF) attacks. You must manually handle security and ensure that your API endpoints are protected.
Some mitigation strategies include:
- To prevent XSS attacks:
- Always sanitize and escape any user input to prevent malicious scripts from being injected into your web pages.
- Implement a strong Content Security Policy (CSP) header to restrict the types of scripts that can be executed on your site. For example, you can disallow inline JavaScript execution and only allow trusted script sources. For example,
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.com; object-src 'none';
- Always sanitize data before injecting it into the DOM (for example, using libraries like DOMPurify to sanitize HTML content).
- To handle token expiration and rotation:
- Check the token's
exp
claim on every request to ensure the token is still valid and hasn't expired. - You can implement automatic token refresh logic using refresh tokens. The refresh token can be securely stored in an
HttpOnly
cookie, and it can be used to obtain a new access token when the current one expires. This prevents you from keeping long-lived JWTs inlocalStorage
and provides an additional layer of security. - After each use, issue a new refresh token and invalidate the old one. This reduces the risk of a stolen refresh token being used to generate new access tokens.
- When the JWT expires, you can either remove the token from
localStorage
and log the user out or automatically refresh the token by requesting a new one from your server.
- Check the token's
- To secure network traffic:
- Always serve your application over HTTPS to encrypt the communication channel and protect the token from MITM (man-in-the-middle) attacks.
- Ensure that your JWT is sent in the
Authorization
header over a secure connection to avoid interception.
Storing JWT in session storage
Session storage is similar to local storage but only persists for the duration of the page session. Once the browser or tab is closed, the data is cleared.
When the user logs in and the server provides a JWT, store it in the browser's local storage:
The token can be retrieved later when making API requests and then added manually to each API request in the Authorization header:
Like local storage, session storage is vulnerable to cross-site scripting (XSS) attacks. The difference is that it is cleared automatically, which adds an extra layer of protection by reducing the window of opportunity for a token to be stolen. However, this doesn't eliminate the security concerns around XSS.
While sessionStorage
provides a session-based storage solution, there are still security risks, particularly related to cross-site scripting (XSS) attacks, similar to localStorage :
- Vulnerability to XSS attacks.
- No built-in token expiry handling.
- No protection against Man-in-the-Middle (MITM) attacks.
- No protection against cross-site request forgery (CSRF) attacks.
For details on these risks, and mitigation steps if you choose to use it, see the previous paragraph on localStorage
.
Storing JWT in-memory
In-memory storage involves storing the JWT in the browser's memory, typically within JavaScript variables.
This is considered one of the most secure options because the JWT is not stored in persistent storage (like local or session storage), meaning it is less likely to be stolen by an attacker.
The main downside is that the token is lost when the user refreshes the page or navigates to a new page. It can also lead to poor user experience:
- In-memory storage is wiped out when the page is refreshed or the browser tab is closed. This means the user must authenticate again after refreshing or closing the page.
- If a user opens the application in multiple tabs, each tab will have its own in-memory state for the JWT, which could lead to discrepancies in the user's authentication state between tabs. One approach to allow tab sharing is to store session-related state in Session Storage or IndexedDB, but these still present security trade-offs (e.g., vulnerability to XSS).
Here's a basic example of how you can store and use a JWT in memory for making authenticated requests:
In-memory storage is typically used for short-lived sessions. For longer sessions (e.g., hours or days), a refresh token mechanism would be necessary to provide a seamless experience, but that requires careful management of refresh tokens (preferably in HttpOnly
cookies).
If you are storing JWT in memory, make sure that the JavaScript environment is secure. If your site is vulnerable to XSS attacks, attackers may still be able to execute malicious scripts that could steal the JWT from memory.
Storing JWT in mobile apps
SharedPreferences (for Android) or NSUserDefaults (for iOS) are generally not encrypted and are easily accessible by malicious apps or through rooting/jailbreaking. Do not store sensitive tokens in these places. Mobile operating systems provide hardware-backed credential storage inaccessible to app processes. Use them instead.
Storing JWT in Android
In Android, you should first create an encryption key that will be used to encrypt and decrypt the JWT. This can be done using the Android Keystore system.
Then you should encrypt the JWT before storing it:
To retrieve the JWT, you must decrypt it using the Keystore. You can retrieve the encrypted data, extract the IV, and use the Keystore to decrypt it:
As of Android 10, you can also use EncryptedSharedPreferences
. This method handles encryption and key management automatically, making it an easier option for secure storage. Using EncryptedSharedPreferences
is an easier and more secure option compared to manually handling encryption with the Keystore.
Here's a simple example using EncryptedSharedPreferences
:
Storing JWT in iOS
To safely store a JWT in an iOS app, you should utilize the Keychain. The Keychain is a secure storage mechanism provided by Apple that encrypts data and ensures that it is protected, even if the device is compromised or the app is uninstalled.
First, you need to import the necessary framework and set up the Keychain wrapper.
Then, create a Keychain helper class that abstracts away the complexity of using the Keychain.
Once you have the KeychainHelper
class, you can use it to store, retrieve, or delete the JWT token securely.
To add an additional layer of security, you can encrypt the JWT before storing it in the Keychain. This ensures that even if the Keychain is compromised, the JWT is still not easily accessible.
Here is a basic example on how to encrypt the JWT before storing it.
You can also consider using Face ID or Touch ID to add biometric authentication when accessing the stored JWT, especially for sensitive operations.
Best practices for storing JWTs securely
While there are several storage options, the key to ensuring JWT security lies in using the right combination of storage mechanisms and taking steps to mitigate vulnerabilities. Here are some best practices for keeping your JWTs secure:
- Use secure transport: Always serve your application over HTTPS. This ensures that data, including JWTs, is encrypted in transit and cannot be intercepted by attackers. Never store or send sensitive data over unencrypted HTTP, as this exposes your tokens to potential eavesdropping and man-in-the-middle attacks.
- Short-lived access tokens: Use short expiration times for access tokens and rely on refresh tokens for session persistence.
- Rotate refresh tokens: Implement refresh token rotation to mitigate the risks of token theft.
- Use secure cookies:
- Set the
HttpOnly
flag to ensure that the token is only accessible by the server and not by malicious JavaScript code. - Set the
Secure
flag to ensure that the cookie is only sent over HTTPS, preventing man-in-the-middle (MITM) attacks. - Set the
SameSite
flag to prevent cross-site request vulnerabilities.
- Set the
- Enable CORS: Ensure Cross-Origin Resource Sharing (CORS) is correctly configured for secure cross-origin requests.
- Implement token expiration and revocation: Ensure that JWTs have an expiration time (
exp
claim) and implement a mechanism for revocation if necessary. Expired tokens should no longer be accepted, and revocation lists or blacklists should be in place for tokens that need to be invalidated before they expire. - Consider in-memory storage for sensitive data: For particularly sensitive JWTs, consider storing them in memory rather than in persistent storage like local or session storage. This reduces the risk of the token being stolen through storage mechanisms that are accessible by JavaScript. However, bear in mind that in-memory storage can complicate user experience (e.g., requiring re-authentication after a page refresh).
- Prevent XSS attacks: No matter where you store your JWT, preventing cross-site scripting (XSS) attacks is crucial. XSS attacks occur when an attacker injects malicious scripts into your website that can steal tokens from local or session storage. To prevent XSS:
- Sanitize and validate all user inputs.
- Implement a Content Security Policy (CSP) that restricts the execution of unauthorized scripts.
- Avoid inline JavaScript and use external scripts with integrity checks.
Conclusion
JWTs are a powerful tool for managing authentication and authorization in modern web applications. However, their security is only as good as the way they are stored. By following best practices like using secure cookies, protecting against XSS attacks, implementing short-lived tokens, and using HTTPS, you can keep your JWTs secure and ensure that your application remains safe from common attacks.
Storing tokens securely is an ongoing process, so always stay informed about new threats and security updates to maintain the integrity of your JWT-based authentication system.