JWT Security: 6 Common Failures in Open Source

SkylerDev Intermediate 1h ago Updated Jul 25, 2026 432 views 7 likes 3 min read

Most developers treat JWTs like a "set it and forget it" feature, but after auditing 12 open-source Node.js projects—ranging from 2k-star templates to actual production boilerplates—it's clear that "copy-pasting from a tutorial" is the primary security strategy. The developers are talented, but the implementations are riddled with the same six amateur mistakes.

The "Tutorial Trap" Hall of Fame

1. The "Secret" Secret


I found three separate projects using literally "secret" as their signing key.

const token = jwt.sign({ userId: user.id }, "secret", { expiresIn: "1h" });

A 6-character ASCII string provides about 42 bits of entropy. A GPU cluster will chew through a dictionary attack on this in milliseconds. If your secret is a word found in a dictionary, you don't have a secure token; you have a polite suggestion of security. Use a 256-bit cryptographically random key.

2. Using jwt.decode() for Authorization


This is a classic "it works in testing" disaster. I saw this in actual production middleware:

// DANGEROUS: No signature verification
const decoded = jwt.decode(req.headers.authorization.split(" ")[1]);
if (!decoded.userId) return res.status(401).send("Unauthorized");

jwt.decode() just Base64-decodes the string. It doesn't care if the signature is valid, expired, or written in crayon. An attacker can forge any payload they want, and this code will happily let them in.

The Fix: Use jwt.verify().

const decoded = jwt.verify(token, process.env.JWT_SECRET, { algorithms: ["HS256"] });

3. Leaving the Algorithm to Chance


Even when using verify(), many skip the options object:

// Missing algorithms option
jwt.verify(token, secret);

If you don't explicitly define { algorithms: ['HS256'] }, the library trusts the alg header inside the token. A savvy attacker can change the header to alg: none and send an empty signature. Some libraries will see "none," see that the signature is indeed empty, and declare the token valid. It's essentially letting the intruder choose the lock for your front door.

4. Git-Committing the "Secret"


Nothing says "professional" like a config file committed to a public repo that looks like this:

// Found in config.js, committed to a public repo
module.exports = {
 jwtSecret: "productionsecretdonotshare2024",
 database: process.env.DATABASE_URL
};

The developer even added a comment saying "do not share." The irony is palpable. Once a secret hits Git history, it's compromised forever. Deleting the line in a new commit doesn't help; the secret is still sitting there in the commit history for any bot to scrape.

5. LocalStorage Obsession


The frontend "standard" seems to be dumping tokens into localStorage:

localStorage.setItem("token", response.data.token);

localStorage is a playground for XSS. One unsanitized input field or one compromised npm dependency, and your user's token is exfiltrated to a remote server. Switch to httpOnly cookies. Since JavaScript can't touch them, XSS attacks can't steal them.

6. The Immortal Token


I saw multiple implementations completely omitting the expiresIn parameter:

// No expiresIn — this token is basically a lifetime achievement award
const token = jwt.sign({ userId: user.id }, secret);

A JWT without an exp claim never expires. If that token leaks into a log file or a browser cache, it's valid until the end of time (or until you rotate the global secret and kick everyone out). Set access tokens to 15 minutes and implement a proper refresh token pattern for long-lived sessions.

The "Don't Get Hacked" Checklist

If you're doing a deep dive into your own AI workflow or building a real-world LLM agent that handles auth, run through this:

  • Entropy: Is the secret 256-bit and generated via CSPRNG?
  • Environment: Is the secret in a .env file (and .gitignore-ed), not in config.js?
  • Verification: Is jwt.verify() used exclusively for auth checks?
  • Explicit Algs: Does every verify() call include { algorithms: ['HS256'] }?
  • Storage: Are you using httpOnly cookies instead of localStorage?
  • Expiration: Does every token have a reasonable exp claim?
AI ProgrammingAI Codingwebdevsecurityjwtsecret

All Replies (2)

T
Taylor27 Intermediate 9h ago
Had a similar headache with a project last year where the secret was just 'secret'. Took forever to find why the tokens were so easy to spoof.
0 Reply
N
Nova25 Novice 9h ago
Which specific repos did u audit? Hard to trust these claims without seeing the actual codebase examples or a list of the projects.
0 Reply

Write a Reply

Markdown supported