← Back to blog

JWT Authentication Mistakes That Ship to Production

JWTs are everywhere because they are convenient. That same convenience hides a set of mistakes that are easy to make and hard to notice, because the app works fine right up until someone forges a token.

Decode is not verify

The most dangerous JWT bug is calling decode() when you meant verify(). Decode reads the token's claims without checking the signature, which means it trusts a token anyone could have written. If your auth trusts decoded claims, your auth is theater.

The weak-secret problem

A JWT is only as strong as its signing secret. A short secret, a guessable one, or the classic fallback process.env.JWT_SECRET || "secret" means an attacker can sign their own valid tokens. Use a long, random secret, and never ship a default.

If a user can change their own token's contents and your server believes it, you do not have authentication.

Where you store the token matters

Putting a JWT in localStorage makes it readable by any script on the page, which turns a single cross-site scripting bug into full account takeover. HTTP-only cookies with the Secure and SameSite flags are the safer default.

Expiry and revocation

Tokens with no expiry live forever. A leaked token you cannot revoke is a permanent backdoor. Set short lifetimes, refresh deliberately, and have a way to invalidate. Auth is the one area where "it works" and "it is safe" are very different claims, so scan for these specific patterns before you ship.

Try it free
IOnclad

The pre-launch security scanner that answers "Am I safe to ship?" in under a minute. Runs 100% offline, free to start.