Security

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

1

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.

✓ Dojwt.verify(token, SECRET_KEY) // throws if invalid, returns decoded payload if valid
✗ Don'tjwt.decode(token) // reads the payload with zero verification — never use this to authorize anything
2

Keep 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.

✓ Doexp: Math.floor(Date.now() / 1000) + 15 * 60 // 15-minute expiry
3

Use 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.

4

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.

5

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.

6

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.

✓ Dojwt.verify(token, SECRET_KEY, { algorithms: ['HS256'] }) // explicitly locked to one algorithm
7

Validate 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.

8

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.