JWT Pitfalls I Actually Ran Into (Building an API Gateway and a Carpooling App)
JWTs look deceptively simple. Decode a token, check a signature, trust the payload, move on. It's the "move on" part that gets people.
I've implemented JWT auth twice now — once as middleware in Aegis, an API gateway I built with a plugin-based architecture, and once in Wasselni, a carpooling platform. Here are the pitfalls that actually mattered, not the generic "JWT is bad, use sessions" takes you see everywhere.
1. The algorithm confusion trap
JWTs let the token itself declare which algorithm was used to sign it (the alg header). If your verification code blindly trusts that header instead of pinning the expected algorithm, you've handed an attacker a way to potentially downgrade your signing scheme — including the infamous alg: none case, where some libraries will accept a token as "verified" with no signature at all if you don't explicitly guard against it.
The fix: always specify the expected algorithm explicitly when verifying, never read it from the token. Most modern JWT libraries support this — you pass the algorithm(s) you accept as a parameter to the verify call instead of letting the library infer it from the token.
2. Treating the payload as safe to trust blindly
A JWT's payload isn't encrypted — it's base64-encoded and signed. Anyone can decode and read it (try it on jwt.io). The signature guarantees the payload wasn't tampered with, not that it's secret.
In a gateway context like Aegis, this matters even more — the gateway is the thing forwarding requests to internal services based on what's in that token. If those downstream services also blindly trust decoded claims without re-verifying anything, you've built an implicit trust chain that's only as strong as its weakest link.
The fix: never put sensitive data (passwords, full PII, secrets) in the payload. And if multiple services are involved, be explicit about which layer is responsible for verification — don't assume "the gateway already checked it" is enough context for a service to skip its own validation of what the token grants access to.
3. Long-lived tokens with no revocation story
A JWT's whole appeal is that it's stateless — the server doesn't need to look anything up to verify it's valid. That's also the problem: if a token leaks or a user needs to be logged out immediately (password change, account compromise), there's no built-in way to invalidate a JWT before it naturally expires.
The fix that actually works in practice: short-lived access tokens (minutes, not days) paired with a refresh token that is checked against a store server-side. That gives you statelessness for the common case (verifying access) while still having a kill switch for the rare case (revoking access).
4. Not handling clock skew
If your auth server and your resource server aren't perfectly clock-synced (and in a distributed system, they often aren't), a token can appear expired or "not yet valid" a few seconds early or late even though it's actually fine. This is a subtle one because it doesn't show up in dev — it shows up in production, intermittently, and looks like a flaky bug.
The fix: most JWT libraries support a small clock tolerance/leeway setting (a few seconds) specifically for this. Use it.
5. Where you store the token client-side
This one's more about the client than the token itself, but it's the pitfall that gets talked about the least in backend-focused JWT content. Storing a JWT in localStorage makes it readable by any JS running on the page — including injected scripts from an XSS vulnerability elsewhere in your app. httpOnly cookies aren't readable by JS at all, which closes that specific hole, but open you up to CSRF instead if you're not careful with SameSite settings.
The fix: there's no universally "correct" answer here — it's a genuine tradeoff, and the right choice depends on your app's specific risk surface. What matters is picking one deliberately instead of defaulting to localStorage because it's the easiest to reach for.
None of these are exotic. They're the kind of thing that's easy to know about in the abstract and still get wrong in the specifics — because the specifics depend on your actual architecture (gateway vs. monolith, one service vs. many, how sensitive the data behind the token actually is).
If you're implementing JWT auth for the first time, I'd rather you spend time thinking through these five than reaching for the first "add JWT auth in 10 lines" tutorial you find.
Top comments (0)