Is JWT Safe or Is It Vulnerable?
Written by Evren BalPublished Updated · 7 min read

💡 Quick Summary (TL;DR):
- The trade-off: A JWT can be invalidated, but a denylist, introspection, or session-state check gives back some of the simplicity that made a stateless token attractive.
- The security boundary: A signature protects integrity, not confidentiality or current authorization. The application still has to validate the expected token and make the access decision.
- The decision: Short-lived access tokens and protected refresh tokens can be appropriate. If every request already depends on server-side session state, a conventional session cookie may be the simpler fit.
Let's start by explaining what JWT (JSON Web Token) stands for. However, the main focus of this post is not about how JWT can increase security, but rather how it can harm it if not applied with caution.
JWT is an open standard used for creating access tokens. The server will often create the token and send it to the client. An integrity-protected JWT can use a shared secret and HMAC, or an asymmetric signature such as RSA or ECDSA. In the latter case, the issuer signs with a private key and the recipient verifies with a public key. A JWT is therefore not always “signed with the server's private key.”
When a signature validates, the receiver knows that an unauthorized party did not alter the token and that it came from a holder of the relevant key. That does not make a signed JWT encrypted. Anyone who can read the token can normally read its payload, so it is a poor place for secrets. Nor does a valid signature establish that the account is still active, its permissions are unchanged, or the user may access the resource in this request. Those remain application decisions.
What JWT Is Not
Contrary to popular belief, JWT is not a magic security bullet. If it is applied carelessly, it can introduce serious vulnerabilities. Signature verification is not a substitute for authorization in a critical operation.
Consider this common line of reasoning:
"The information inside the JWT is signed and reliable. There is a username 'evrenbal' in the token, so my server verified that he is indeed 'evrenbal' when he logged in. What could possibly go wrong?"
Imagine a user discovers that their password has been stolen, or realizes they forgot to log out on a public computer they used recently. Naturally, they will immediately log in to their account, change their password, and assume the problem is solved.
The problem is that, if the backend checks only that the token is structurally valid and not expired, changing the password does not invalidate the active token on that public computer. The attacker still has access until it expires or the application checks some revocation state.
There are two distinct checks here. The first is cryptographic validation. The second is the application's decision about this request. Do not let the token header choose the algorithm or key: configure an allowed algorithm set and bind each key to its intended algorithm. Validate every cryptographic operation, then check the expected issuer, audience, purpose, and, where needed, token type. Finally, validate received claims in the application context and make the current authorization decision. RFC 8725 sets out these JWT practices, including the defenses against alg: "none" and key-confusion failures.
The Expiration and Refresh Token Dilemma
You might argue:
"I can set a very short validity period (expiration) on the JWT to fix this!"
Keeping an access token short-lived and using a longer-lived refresh token can be appropriate. Issuing a refresh token should still be a risk decision, not a default.
In this way, while ensuring that an access token becomes ineffective quickly if stolen, you can automatically issue a new access token without forcing the user to log in again.
However, there is a catch. To validate the long-term refresh token and allow prompt revocation, the authorization system needs state in a database or cache such as Redis.
If the incoming refresh token is valid and matches the one stored in your database, you generate a new access token. If the user changes their password, you can delete or revoke the active refresh tokens and invalidate those sessions. You can also offer "Log out of all other devices." For public clients, current OAuth guidance calls for refresh-token rotation or sender-constrained refresh tokens, along with expiry and revocation. RFC 9700 recommends deciding whether to issue refresh tokens based on risk and describes both replay-detection approaches.
Did We Really Solve the Problem?
Not entirely. During the active window of the short-lived access token, you still have no way of knowing if the token has been hijacked. You will only detect it and block the user once the access token expires and they attempt to request a new one using the refresh token.
If you reduce the access token lifetime to just 1-2 minutes to minimize this window, the client will renew it more often. That may be the right trade-off. But if every request also performs a database or cache check, the “stateless” benefit that justified JWT has already become much smaller.
Safer Implementation Patterns
If you must use JWT, consider implementing these strategies:
Pattern 1: Password Age Verification (Quick Fix)
When verifying a JWT, check its creation timestamp (iat) against the user's last_password_change date stored in a fast cache (like Redis). If the password was changed after the token was issued, deny the request.
- Result: The user is instantly logged out from all devices upon a password change, though this does force a logout on their legitimate devices as well.
Pattern 2: Session Tracking Per Login
Every time a user logs in, generate a unique session ID (e.g., "ABCDE"). Store it in a fast-access cache such as Redis and embed it in the JWT payload. On every request, compare that session ID with the active sessions in the cache. IP address, user agent, and operating-system details can help a user recognize a session or flag an unusual change. They are supporting signals, not cryptographic device binding or a reliable identity proof.
- Result: When the user changes their password, you can clear all active sessions from Redis, instantly invalidating all issued JWTs. You can also build a "Devices" management page allowing users to revoke specific sessions individually.
Browser Storage Is a Separate Decision
Keeping a JWT in localStorage makes it available to JavaScript running on the same origin. An XSS flaw can therefore expose the token. HttpOnly, Secure, and appropriately chosen SameSite cookies, or a browser-facing BFF architecture, can reduce that exposure. They also make the browser send credentials automatically, so CSRF protections and the rest of the session design still need deliberate attention. SameSite is defense in depth, not the whole CSRF strategy. OWASP's Session Management Cheat Sheet explains the boundary.
Is JWT Actually Necessary?
If you are already storing session IDs (ABCDE) in a cache, querying it on every request, and managing session lifetimes on the server, you have built a stateful system.
If your backend is stateful, why carry the overhead of a large base64-encoded JWT payload and include JWT parsing libraries? In most traditional monolithic or single-domain web applications, a simple, lightweight secure cookie containing a session ID is a much simpler, more secure, and battle-tested choice.
JWT is an exciting technology, and when developers first discover it, they often want to use it for everything. Before integrating it, ask whether it really beats a conventional session cookie for this client, trust boundary, and revocation requirement.
This is also a useful guardrail when an AI agent helps build an API. JWT should not be the default chosen by a prompt. Make the agent explain the client type, the revocation need, the trust boundary, and why a JWT is preferable to a server session. I discuss how to preserve those decisions in AI-assisted REST API development.
If this article was useful
Linking to it from a relevant page on your website or sharing it on social media genuinely helps it reach more people. Thank you for your support.
Linking and brand guidelines →About this article
Change history
- · Substantive update — Cleaned Turkish-English translation leakage, added common JWT security failure modes, and corrected variable and verb usage.
- · Substantive update — Updated the signing, validation, refresh-token, revocation, browser-storage, and device-signal guidance; added the API-development decision link.
