How to Properly Secure Your JWTs
Explore best practices and techniques to protect your JWTs from common security vulnerabilities and keep your applications secure.
Overview
Most JWT vulnerabilities come from implementation choices: accepting weak or unexpected signing algorithms, storing tokens insecurely on the client, skipping expiration and revocation, or embedding sensitive data in the payload. This guide covers the technical controls that prevent each of these, plus where to store a JWT client-side, which is the single most common question we get asked. By the end of this post, you should have a better understanding of how to properly secure your JWTs and ensure the integrity of your web application.
What is a JWT and how does it work?
A JSON Web Token (JWT) is a compact, signed data structure used to prove identity and permissions between a client and a server without a round-trip to a session database. They are a popular method for managing user authentication and authorization in web applications. JWTs scale well across APIs, microservices, and mobile apps but it also means the token itself becomes the single point of trust. If it's forged, stolen, or replayed, whatever it grants access to is compromised. A JWT's payload is encoded, not encrypted. Anyone who intercepts a token can read every claim inside it in plain text. The signature proves the token hasn't been tampered with but it does nothing to hide what's in it.
Key points:
- Use strong algorithms: RS256 or ES256
- •Set short expiry times (15–60 minutes) and use refresh tokens
- Store JWTs in Http Only cookies, not local storage
- Validate the ‘alg’ field server-side and reject ‘none’ algorithm attacks
- Implement token revocation (blocklist or short TTL + rotation)
JWT stands for JSON Web Token; it is an open standard format for securely transmitting information between parties as a JSON object. JWTs consist of three parts separated by a period:
- Header: describes the type of token and the algorithm used to sign it.
- Payload: contains the information that is being transmitted.
- Signature: used to verify the authenticity of the token.
Let’s take a look at the following example of a JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTY3ODk2NDY4MCwiZXhwIjoxNjc4OTY4MjgwfQ.tQC7xwGRKg-p4bojB2mS7A-26_wNLwlrTeiInKH6LLc
If we break this down into its parts, the first part (eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9) is the header, and it decodes to:
{
“alg”: “HS256”,
“typ”: “JWT”
}
The second part (eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTY3ODk2NDY4MCwiZXhwIjoxNjc4OTY4MjgwfQ) is the information being transmitted, and it decodes to:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"iat": 1678964680,
"exp": 1678968280
}
“iat” is the time the token was issued, and “exp” is when the token would expire. These are some of the standard fields you would find in JWTs.
The last part (tQC7xwGRKg-p4bojB2mS7A-26_wNLwlrTeiInKH6LLc) is the signature.
JWTs are often used in authentication systems to generate tokens that can be passed between different systems or services to authenticate the user and provide access to resources. JWTs are self-contained, which means that all the information needed to validate the token is contained within the token itself. This makes them easy to use and implement, as well as secure, since they are signed and can be verified by the receiving party.
Validate the signing algorithm
JWT headers declare which algorithm was used to sign the token. Some libraries, if not configured correctly, will honor whatever algorithm the header claims, including none, which some implementations historically accepted as "no signature required." An attacker who can submit an unsigned or re-signed token with alg: none, or who switches an RS256 (asymmetric) token to HS256 (symmetric) and signs it with the server's public key treated as a shared secret, can forge valid-looking tokens.
What to do:
- Explicitly allowlist the algorithm(s) your application accepts server-side. Never read the algorithm from the incoming token and use it to decide how to validate.
- Reject
noneoutright at the library configuration level, not just in application logic. - If you use asymmetric signing (RS256/ES256), make sure your verification code is configured to only ever validate with the public key for that specific algorithm family — never fall back to symmetric verification.
How are JWTs different from cookies?
Cookies are pieces of information that are stored on a user's computer by a web application, typically used to store user preferences, session data, and other information. JWTs and cookies are both commonly used for client-side storage and transmission of data, but they are different in some ways.
Cookies
JWTs
Security
Can be easily manipulated without detection if not properly secured.
Digitally signed and can be validated on the server. Manipulation can be detected.
Size
Limited to 4KB.
Can contain much more data, up to 8KB.
Dependency
Often used for session data on the server-side. The server needs to store the session map.
Contains all the necessary information in the token. Doesn’t need to store data on the server.
Storage Location
Browser cookie jar.
Local storage or client-side cookie.
Where are JWTs stored?
JWTs are typically stored on the client side, either in local storage or in a client-side cookie.
Local storage is a type of web storage that allows web applications to store key-value pairs in the browser, which can persist even after the browser is closed or the computer is turned off. JWTs can be stored in local storage using JavaScript code that sets or retrieves the token.
Client-side cookies are another way to store JWTs on the client side. To store a JWT in a cookie, the server can set a cookie with the token value and expiration date, and the client-side code can retrieve and send the cookie back to the server on subsequent requests.
Why do JWTs suck?
While JWTs themselves are not inherently flawed, how they are implemented by developers can lead to security weaknesses in applications. There are a few common mistakes that developers can make when using JWTs. When you pair these mistakes with other vulnerabilities, they can cause significant damage to an application. For example:
- Cross-site scripting (XSS): When JWTs are used in conjunction with XSS vulnerabilities, an attacker can potentially steal a user’s JWT and use it to gain unauthorized access to the application.
- Authentication: If a signature exclusion vulnerability exists, an attacker can modify the contents of a JWT without invalidating the signature, allowing them to potentially gain access to sensitive information or perform unauthorized actions within the application.
Let’s look into some more challenges one would face while using JWTs.
Storage
The storage method chosen by developers can significantly impact the security of the JWTs and the application. Storing JWTs in memory is an effective approach because the data is only accessible within the browser's memory space. This means that if the browser is closed or reset, the JWT is lost. However, this approach may not be ideal for all applications.
On the other hand, session storage provides a more persistent storage solution for JWTs, as the data is stored within the user's session. However, session storage is still vulnerable to XSS attacks. An attacker can steal JWTs and gain unauthorized access to the application.
The best option is to store JWTs in a cookie with proper flags set, such as httponly, secure, and samesite. This is the only place where the JWT cannot be targeted by cross-site scripting (XSS) attacks.
Sensitive data
Unlike cookies, which do not contain sensitive information, JWTs can contain a name and other sensitive information that should not be exposed to unauthorized parties. Developers need to be aware of what information they are storing in JWTs and ensure that it's information they can tolerate exposing to others.
JWTs are not meant to be modified, but they can be decoded to view the information inside them. Modifying a JWT is essentially an attack, and developers need to check if their application verifies the integrity of the encoded JWT beyond its initial issuing.
Credit card information, SINs, social security numbers, and other personally identifiable information should not be stored in JWTs. Although this is not as common anymore, it's still a good practice to follow through with education and awareness.
Invalidating JWTs
Developers don’t always remember to invalidate JWTs when users log out or change their passwords. This means that if a user logs out of an application, the JWT should become invalid, and if someone tries to use the same JWT, it should not work. However, if developers do not invalidate the JWT, it can cause issues.
Imagine a library where a user logs into an app that uses JWTs. The next person comes in and uses the same computer without closing the browser. They can then go to the browser history, find the same site, and use the JWT key. This poses a risk of account takeover, especially in shared spaces.
Sometimes changing your password does not invalidate the old JWTs. If your account has been compromised, you would hope that the hacker loses access, but without proper invalidation of JWTs, they can still have access to your account through the old JWTs. All in all, invalidating JWTs is a crucial implementation when using JWTs.
While JWTs themselves are not inherently insecure, developers need to understand the potential risks and vulnerabilities associated with their use. By following best practices for securing JWTs, such as the refresh token feature, developers can minimize the risk of security issues arising in their applications. Let’s look at some best practices to follow when using JWTs.
Pros and Cons of JWTs
JSON Web Tokens are widely used because they support stateless authentication and scale well across distributed systems, microservices, and partner integrations. Since the server generates a token after a user successfully logs in, applications can avoid repeated database lookups. A JWT access token is self-contained, carrying JWT claims inside a JSON object, which makes authorization checks fast. Tokens are digitally signed using a cryptographic algorithm, helping preserve the JWT token’s integrity.
However, JWT implementation comes with tradeoffs. If tokens are stolen, attackers may forge them or reuse them until they expire. Poor JWT token security, weak signing algorithm choices, or improper key handling increase risk. Long-lived access tokens can be hard to revoke, and mismanaged custom claims may expose sensitive data if not carefully designed.
Benefits of using JWT tokens
What is a JWT token? A JSON Web Token (JWT) is a compact, self-contained format for securely transmitting information between parties. Because a JWT header and payload are digitally signed using a secret key or a public key and private key pair, recipients can verify authenticity without calling the issuing service. This supports stateless authentication, where the server processes requests without storing session state.
JWT authentication improves performance because access tokens travel with each request, allowing services to validate identity quickly. A JSON Web Token type can include custom claims tailored to application needs while remaining portable across services. When implemented with JWT security best practices, such as strong signing algorithm selection and proper key management, JWTs simplify distributed authorization and improve reliability at scale.
Best ways to securely implement JWTs
JSON Web Tokens (JWTs) can be a powerful tool when implemented securely. Here are some best practices for implementing JWTs:
- Use strong algorithms like HMAC-SHA256 or RSA to sign and encrypt your tokens. Avoid using weak algorithms like HMAC-MD5.
- Set an expiration time for the JWT to limit its validity period. This ensures that even if a JWT is compromised, it won't be valid for an extended period. Use a short-lived token life ideally no more than 15 minutes.
- Set refresh token features to extend the session duration, which allows users to fetch new JWT tokens for an extended period. For example, when the JWT expires, the refresh token automatically updates and refreshes the token to allow the customer to have a seamless experience.
- Refresh tokens also require specific security measures; the refresh token needs to be expired after a reasonable period (a few hours to a day).
- Avoid storing sensitive information in JWTs. Instead, use a separate database to store such information. Only include information in the JWT that is required for authentication or authorization purposes.
- Implement a mechanism to revoke JWTs when they are no longer needed, such as when a user logs out or changes their password. This ensures that even if a JWT is stolen or compromised, it won't be valid for an extended period. Developers need to code additional backend functionality that will invalidate JWTs instantly.
- When receiving a JWT, confirm that it is signed and has not been tampered with using a digital signature.
- Use HTTPS to ensure that the JWT is transmitted securely between the client and server.
- Store JWTs securely using mechanisms like browser cookies with the HttpOnly, Secure flag, and SameSite attributes set. This ensures that the JWT can't be accessed by JavaScript, and it's only sent over HTTPS connections. Do not store JWTs in local storage.
- Implement mechanisms to prevent cross-site scripting (XSS) attacks, as they can be used to steal JWTs. Use Content Security Policy (CSP) headers to limit the sources of scripts that can execute on your site.
- It's important to never render the token on screen, in URLs, and/or in source code. Doing so can allow an attacker to easily obtain JWTs and impersonate users.
- The "none" algorithm does not provide any signing or encryption, which leaves the token open to tampering. Avoid supporting this algorithm in your application.
By following these best practices, you can securely implement JWTs in your application and reduce the risk of JWT-related attacks.
What is actually found in penetration tests
JWT misconfigurations are one of the more consistent findings across manual application penetration tests because the failure modes above are easy to miss in code review and invisible in automated scans. A scanner can confirm a token is present and formatted correctly; it generally can't tell you whether your revocation story falls apart under a stolen-refresh-token scenario, or whether an internal microservice is quietly trusting a token's alg header.
Conclusion
Securing JWTs is a critical aspect of web application development. Despite their many benefits, JWTs can pose security risks if they are not properly implemented and secured. But good for us, developers can take steps to mitigate potential threats and ensure the security of their applications.
In this post, we went through some of the best practices to secure JWTs. By following these best practices, developers can ensure that their JWTs are properly secured and that their web applications remain safe from unauthorized access and potential attacks. It is important to recognize that there is no one-size-fits-all approach to securing JWTs. Therefore, in addition to the above-mentioned practices, developers must consider their specific use case for JWT security and take a proactive approach to secure their applications.
JWT misconfigurations are a recurring finding in the Software Secured application penetration tests. If you want to know whether yours would hold up, book a consultation.
FAQ
Is JWT secure?
JWTs are a secure mechanism when implemented correctly. They are not secure by default against algorithm confusion, insecure storage, or missing revocation; those depend entirely on implementation choices.
Where should I store a JWT?
For browser applications, store the access token in memory and the refresh token in an HttpOnly, Secure, SameSite=Strict cookie. Avoid localStorage for anything beyond short-lived, low-privilege tokens.
How do I secure a JWT on the client side?
Keep tokens out of persistent, JavaScript-accessible storage, use short expirations, validate the signing algorithm server-side on every request, and never store sensitive data directly in the payload.
Can a JWT be revoked before it expires?
Not natively. You need either a server-side refresh token store you can invalidate, or a denylist of revoked token IDs checked on each request. Short access-token lifetimes make this less urgent but don't eliminate the need for a plan.
What's the best way to store a JWT token in local storage?
There isn't a fully safe way to store a sensitive JWT in localStorage because it's readable by any script that runs on the page, including injected malicious scripts via XSS. If you must use browser storage for a low-privilege token, pair it with strict Content Security Policy headers to reduce XSS risk, but an HttpOnly cookie or in-memory storage is a stronger default.

.avif)

