A Token That "Just Works" Isn't the Same as a Secure One
JWTs are everywhere — API auth, SSO, session management — because they're simple to generate and easy to pass around. That simplicity is also exactly why they get misused. A JWT can be perfectly well-formed, verify correctly, and still be a security hole, because the format doesn't protect you from bad decisions about what goes inside it or how it's checked.
Here are the mistakes that show up most often — and how to catch them by actually looking at what's inside your tokens.
1. Trusting the alg Header Too Much
A JWT's header names the signing algorithm — HS256, RS256, none. The catch: that field is set by whoever created the token, and some JWT libraries will happily verify a token using whatever algorithm the header claims, including none (no signature at all) if the server-side code doesn't explicitly reject it.
This is the classic "alg confusion" attack: an attacker takes a token meant to be verified with an RSA public key, changes the header to HS256, and signs it using that same public key as an HMAC secret — because public keys are, by definition, public. If your verification code doesn't pin down the expected algorithm, it can be tricked into accepting a forged token.
Fix: always specify and enforce the exact algorithm you expect server-side; never let the token's own header decide how it gets verified.
2. Sensitive Data Sitting in Plaintext
The "signed" part of JSON Web Token only guarantees the payload hasn't been tampered with — it says nothing about who can read it. The header and payload are just Base64URL-encoded, not encrypted. Anyone with the token — a browser extension, a proxy log, a teammate glancing at devtools — can decode it in seconds and read every claim inside.
Tokens with emails, internal role names, permission lists, or anything resembling PII in the payload are handing that data to anyone who intercepts the token, signature or not.
Fix: keep the payload to what's needed for authorization (subject ID, expiry, minimal scopes). If you need to transmit sensitive data, that's what encrypted JWTs (JWE) or a server-side session lookup are for.
3. No Expiry, or an Expiry Nobody Checks
A JWT without an exp claim is valid forever, which means a leaked token — from a log file, a compromised device, an old bookmarked URL — stays usable indefinitely. And even when exp is present, it's only useful if the verification code actually checks it; a surprising number of custom JWT-handling implementations decode the payload and manually pull out claims without running full verification, silently skipping the expiry check entirely.
Fix: always set a short, sensible exp, and confirm your verification path is actually validating it — not just parsing the payload and trusting it.
4. Reusing the Same Secret Everywhere
It's common for a codebase to have one JWT_SECRET env var used for every kind of token — access tokens, refresh tokens, password reset links, email verification tokens — all signed with the same key. If any one of those flows has a vulnerability (say, a reset-token endpoint that leaks a valid signature), an attacker can potentially forge tokens for a completely different flow, since they all trust the same secret.
Fix: use distinct signing secrets (or key pairs) per token purpose, so a compromise in one flow doesn't cascade into every other one.
5. Not Actually Looking at What's Inside Your Tokens
This sounds almost too simple, but it's the root cause behind most of the above: teams generate JWTs, ship them, and never actually inspect what ends up in the payload in production. Claims drift over time as features get added — someone adds a isAdmin boolean "temporarily" for testing and it's still there eight months later, sitting in every token, readable by anyone who decodes one.
The fastest way to catch this is to just decode a real token and read it, the same way you'd review an API response. JWT Decoder on SamToolkit does exactly that — paste a token in and instantly see the decoded header and payload, with nothing sent to a server (it runs entirely in your browser, so you're not pasting production tokens into someone else's backend). It's a fast way to sanity-check exactly what a token exposes, verify the algorithm and expiry are what you expect, and catch stray claims before they become a habit.
The Underlying Principle
JWTs give you integrity (tamper detection) for free. They give you confidentiality and correct verification only if you set them up carefully. Treat every claim you add to a payload as something a stranger might read, treat the alg header as attacker-controlled input, and periodically decode your own tokens to check what's actually shipping — before someone else does it for you.
What's the worst JWT misconfiguration you've caught in a code review? Share it below.