JWT Best Practices
JWTs are easy to implement incorrectly in ways that don't cause obvious bugs — the token still works, authentication still passes — while quietly leaving real security gaps. These are the practices that separate a JWT implementation that merely works from one that's actually safe to run in production.
Last updated 2026-08-21
Always verify the signature server-side before trusting any claim
Decoding a JWT's payload requires no secret and proves nothing about authenticity. Only signature verification — using your server's secret or public key — confirms a token hasn't been tampered with. Reading a role or user-ID claim without first verifying the signature means trusting data that could have been forged.
jwt.verify(token, SECRET_KEY) // throws if invalid, returns decoded payload if validjwt.decode(token) // reads the payload with zero verification — never use this to authorize anythingKeep access tokens short-lived
A JWT can't be individually revoked once issued (without extra infrastructure) — it stays valid until it expires, no matter what happens to the underlying account. Short expiry (typically 15 minutes to a few hours) limits how long a stolen or leaked token remains usable.
exp: Math.floor(Date.now() / 1000) + 15 * 60 // 15-minute expiryUse refresh tokens for long-lived sessions, not long-lived access tokens
Rather than extending an access token's lifetime to avoid re-login, issue a separate, longer-lived refresh token that can be used to obtain new short-lived access tokens. This keeps the actively-used token's exposure window small while still giving users a persistent session.
Never store sensitive data in the payload
The header and payload are Base64URL-encoded, not encrypted — anyone holding the token can read every claim inside it instantly, with no key required. Passwords, full card numbers, or anything genuinely confidential has no business being in a JWT payload.
Store tokens in an httpOnly cookie, not localStorage, for browser apps
A token in localStorage is readable by any JavaScript running on the page — including an injected script from an XSS vulnerability. An httpOnly cookie is inaccessible to JavaScript entirely, which removes an entire category of token-theft risk. Pair it with the Secure and SameSite flags for further protection.
Explicitly set and check the algorithm
A well-known JWT vulnerability involves an attacker changing the header's alg field to 'none' or switching from an asymmetric to a symmetric algorithm, tricking a poorly-configured verifier. Explicitly specify and enforce the expected algorithm on the verifying side rather than trusting whatever the token claims to use.
jwt.verify(token, SECRET_KEY, { algorithms: ['HS256'] }) // explicitly locked to one algorithmValidate the issuer and audience claims
If your token verification only checks the signature, a validly-signed token issued for a completely different application or purpose could still pass. Checking iss (issuer) and aud (audience) claims ensures the token was actually meant for this specific service.
Have a revocation strategy for genuinely sensitive actions
Because JWTs are stateless by design, there's no built-in way to invalidate one before it expires. For sensitive scenarios (a user changing their password, an admin forcibly logging someone out), maintain a short-lived server-side denylist or a token-version claim you can invalidate.
Common Mistakes to Avoid
- ⚠Trusting jwt.decode() output without ever calling jwt.verify()
- ⚠Setting expiry to days or weeks 'so users don't have to log in again' instead of using refresh tokens
- ⚠Storing the JWT in localStorage in a single-page app, exposing it to any XSS vulnerability
- ⚠Putting a password, full credit card number, or other sensitive data directly in the payload
- ⚠Not pinning the expected signing algorithm, leaving the verifier to trust whatever the token's header claims
- ⚠Assuming HTTPS alone makes token storage location irrelevant — HTTPS protects data in transit, not a token already sitting in the browser's JavaScript-accessible storage
Frequently Asked Questions
Are JWTs inherently insecure?
No — JWTs are a sound mechanism when implemented correctly. Most real-world JWT vulnerabilities come from implementation mistakes (skipping signature verification, weak secrets, algorithm confusion) rather than a flaw in the JWT concept itself.
Should I always use a refresh token?
For any application where users expect to stay logged in beyond a short session, yes — it's the standard way to balance short-lived access tokens (safer) with a persistent user experience (convenient), without extending the access token's own lifetime.
Is Base64URL encoding a form of security?
No — it's purely a way to represent the token's parts as URL-safe text. It provides zero confidentiality; anyone can decode it instantly without any key.
What's the safest place to store a JWT in a browser app?
An httpOnly, Secure, SameSite cookie — this keeps the token inaccessible to JavaScript (protecting against XSS-based theft) while still being sent automatically with requests to your own domain.