I Audited 12 Open Source JWT Implementations and Found the Same 6 Mistakes
DEV Community

I Audited 12 Open Source JWT Implementations and Found the Same 6 Mistakes

I spent last month reviewing JWT implementations across 12 open-source Node.js projects on GitHub - ranging from starter templates with 2k stars to production boilerplates used by teams at real companies. I found the same 6 mistakes in almost every one.

None of these projects are bad. The developers are skilled. The mistakes are subtle, copy-paste errors from tutorials that nobody questioned. Here they are.

Mistake 1 - The Secret Is Literally "secret"

I found this in three separate projects:

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

This secret is in every JWT tutorial on the internet. It is in the jwt.io documentation. It is in the jsonwebtoken README. Developers copy it and forget to replace it. A 6-character ASCII secret has approximately 42 bits of entropy. A GPU cluster cracks it from a dictionary in milliseconds. Generate a real secret here - it takes 3 seconds and produces a 256-bit cryptographically random key.

Mistake 2 - jwt.decode() Used in Auth Middleware

// DANGEROUS - this is in a production auth middleware
const decoded = jwt.decode(req.headers.authorization.split(" ")[1]);
if (!decoded.userId) return res.status(401).send("Unauthorized");

jwt.decode() does not verify the signature. It reads the payload regardless of whether the token is valid, expired, or forged. An attacker can craft any payload they want and it will pass this check. The fix is two characters: jwt.verify().

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

Mistake 3 - Algorithm Not Specified in verify()

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

Without { algorithms: ['HS256'] }, the library trusts whatever algorithm is in the token's header. An attacker can create a token with alg: none and an empty signature - and jwt.verify() will accept it. Always specify the expected algorithm explicitly.

Mistake 4 - Secret Committed to Version Control

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

Note the comment - "do not share." The developer knew this was sensitive. But it still ended up committed to a public repository. Once in git history, a secret is compromised permanently. Even deleting the file does not remove it from history. Rotate immediately if this has happened to you.

Mistake 5 - Tokens Stored in localStorage

// Frontend code in multiple projects
localStorage.setItem("token", response.data.token);
// Later:
const token = localStorage.getItem("token");

localStorage is accessible to any JavaScript running on the page. A single XSS vulnerability - an improperly sanitised comment, a compromised npm package, a third-party script - and all stored tokens are exfiltrated. Use httpOnly cookies for authentication tokens. They are invisible to JavaScript, making XSS token theft impossible.

Mistake 6 - Tokens Never Expire

// No expiresIn - token is valid forever
const token = jwt.sign({ userId: user.id }, secret);

A JWT without an exp claim never expires. If it is ever leaked - in a log file, an error response, a frontend cache - it remains valid indefinitely. Always set expiresIn. For access tokens: 15 minutes. For long-lived sessions: use a refresh token pattern, not a long-lived access token.

The Pre-Ship Checklist

Before your next JWT implementation goes to production:

  • [ ] Secret generated with a CSPRNG, minimum 256 bits
  • [ ] Secret in environment variable, not in code
  • [ ] jwt.verify() used everywhere, never jwt.decode() alone
  • [ ] { algorithms: ['HS256'] } explicitly specified
  • [ ] expiresIn set on all tokens
  • [ ] Tokens in httpOnly cookies, not localStorage

I have been using jwtsecretgenerator.com for generating secrets - it runs entirely in the browser using the Web Crypto API, nothing is sent to a server, and it produces the right bit length for your algorithm.

Comments

No comments yet. Start the discussion.