Securing JWTs: Common Mistakes That Make Tokens Stealable
It's easy to focus entirely on JWT signature verification and assume that's the whole security picture — but a properly signed, correctly verified JWT can still be trivially stolen and reused by an attacker if it's stored or transmitted carelessly. Signature verification proves a token wasn't tampered with; it does nothing to protect the token from simply being copied by someone else.
The most common mistake is storing JWTs in localStorage or sessionStorage in a browser application. Both are fully accessible to any JavaScript running on the page — which means if your site has even one XSS vulnerability anywhere, an attacker's injected script can read the token directly out of storage and exfiltrate it, giving them full access to the victim's session with no further effort.
The safer alternative is storing the token in an HttpOnly cookie, which JavaScript cannot read at all, specifically because that flag tells the browser to keep the cookie inaccessible to client-side scripts. This doesn't make XSS harmless — an attacker with script execution can still make requests as the victim — but it does prevent the much simpler attack of just reading and exporting the raw token value.
Token lifetime is another frequently overlooked issue. A JWT with no expiration, or an excessively long one (weeks or months), remains valid for an attacker to use for that entire window if it's ever stolen, with no way to revoke it early since JWTs are typically verified statelessly, without checking back against the server. Short-lived access tokens, paired with a separate refresh token mechanism to issue new ones, limit how long a stolen token remains useful.
Storing sensitive data directly in the JWT payload is a mistake specific to JWTs, because people sometimes forget that the payload is only signed, not encrypted — anyone who has the token, including the legitimate user themselves, can decode and read every claim inside it without needing the secret. Never put a password, a full credit card number, or anything else genuinely sensitive directly into a JWT payload, since it's readable by design, not hidden.
Not validating the algorithm on the verifying side is a subtler, more dangerous mistake. Some JWT libraries, if misconfigured, will accept a token's self-declared algorithm from its header rather than enforcing the algorithm the server actually expects — this has historically enabled a real attack where an attacker changes the algorithm to 'none' or swaps an asymmetric algorithm for a symmetric one, tricking a poorly configured verifier into accepting a forged token. Always explicitly specify which algorithm(s) your verification code accepts rather than trusting the token's own header.
Finally, always verify a JWT's expiration and other standard claims (issuer, audience) even if your library handles this automatically by default in most configurations — a token intended for a different service or environment, if somehow it ends up presented to yours, should be rejected outright rather than accepted just because the signature happens to be valid.
You can inspect any token's header and payload — without needing a working secret key, since decoding doesn't require verification — using our JWT Decoder, which is a fast way to audit exactly what claims and expiration a token actually carries before deciding whether your handling of it is secure enough.
Found this helpful?
SyncTonight's tools and guides are free and always will be. If this post saved you some debugging time, a coffee goes a long way — no pressure, just appreciated.
☕ Buy me a coffee