Most JWT incidents happen because teams skip verification details, over-trust claims, or store tokens unsafely in the browser. Safe JWT handling is less about crypto novelty and more about disciplined validation, short lifetimes, scoped claims, and sane session design.
JWT problems are attractive to startups because the initial implementation looks finished. The token is issued, the frontend stores it, requests carry it, and the product works. The security bugs show up later when someone asks whether the signature is verified, whether the token is meant for this service, whether old tokens can be revoked, and whether the role claims are still true.
The right mindset is to treat JWTs as one component of an auth system, not the auth system itself. A signed token still needs storage discipline, claim discipline, and expiry discipline.
Why JWT Incidents Are Usually Operational
Startup teams rarely get breached because an attacker invented new JWT cryptography. They get breached because the implementation skipped a rule that felt optional: using decode instead of verify, failing to pin the algorithm, trusting a role claim forever, leaving expiry too long, or storing tokens where XSS can steal them.
JWT is particularly unforgiving because the failure often looks legitimate. A forged or stale token is still structurally valid JSON. It still deserializes. Without strict verification, it feels like an authenticated request right up to the point it becomes an incident.
If your product does not truly need stateless bearer tokens, session cookies with a strong server-side session model are frequently the simpler and safer option.
The 9 JWT Mistakes Startup Teams Make
Using decode instead of verify
Criticaljwt.decode() parses a token. It does not prove the token is trustworthy. Signature verification is mandatory.
Allowing implicit algorithms
CriticalYour verifier should pin the expected algorithm. Never rely on whatever the token header claims.
Skipping issuer and audience checks
HighA valid token for some other service should not automatically be valid for this one.
Using weak or reused signing secrets
HighShared low-entropy secrets across environments are one leak away from mass token forgery.
Overlong expiry
HighTokens that live for days or weeks turn account compromise into a lingering operational problem.
Storing tokens in localStorage without thinking about XSS
BrowserIf JavaScript can read the token, any successful XSS can usually steal it.
Trusting role claims forever
AuthzIf roles or memberships change, old tokens can preserve privilege longer than intended without refresh or re-check logic.
No revocation or session kill path
IRIf a token leaks, you need a way to invalidate the session context behind it or reduce its usable lifetime.
Treating JWT as a replacement for authorization checks
A01A user claim is not permission to touch every object. Resource-level authz still matters.
The Difference Between “Works” and “Trustworthy”
The most dangerous JWT code is usually code that works perfectly in development.
import jwt from 'jsonwebtoken';
export function getSession(req: Request) {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) return null;
return jwt.decode(token);
}import jwt from 'jsonwebtoken';
export function getSession(req: Request) {
const token = req.headers.get('authorization')?.replace('Bearer ', '');
if (!token) return null;
return jwt.verify(token, process.env.JWT_SECRET!, {
algorithms: ['HS256'],
issuer: 'custodia.dev',
audience: 'custodia-app',
});
}Verification proves signature integrity and checks that the token was issued for this service under expected conditions. Anything less is faith, not validation.
Even after verification, the application still needs resource-level authorization and session revocation strategy.
When Sessions Are a Better Fit Than JWT
You need fast revocation
Server-side sessions make forced logout and access removal simpler when customers or roles change often.
Your app is mostly browser-based
HttpOnly secure cookies often reduce exposure compared with DIY token handling in JavaScript-heavy frontends.
You do not need cross-service token portability
If the only consumer is your own web app, a session model is often easier to reason about operationally.
You want simpler auth incidents
When a token leak happens, the ability to kill sessions centrally can matter more than the theoretical elegance of stateless auth.