Here is an example of Node.js code that is vulnerable to a JWT authentication bypass:
🥺 Vulnerable Code
const jwt = require("jsonwebtoken");
function getUser(req) {
const token = req.headers.authorization.split(" ")[1];
// Vulnerable: decode only reads the payload, it never checks the signature
const claims = jwt.decode(token);
return { id: claims.sub, role: claims.role };
}
// Almost as bad - no algorithm allowlist, so alg can be swapped:
// const claims = jwt.verify(token, publicKey);jwt.decode() is base64 decoding with extra steps. An attacker mints {"sub":"1","role":"admin"}, sends it with any signature, and becomes an administrator. The commented alternative is the classic algorithm confusion bug: without an allowlist, a token signed with HS256 using the public RSA key as the HMAC secret verifies successfully, and alg: none is accepted by some libraries.
😎 Secure Code
Here is a version of the same code that is secured against a JWT authentication bypass:
const jwt = require("jsonwebtoken");
const VERIFY_OPTIONS = {
algorithms: ["RS256"], // pinned, so alg in the header is ignored
issuer: "https://auth.example.com/",
audience: "api://reports",
maxAge: "15m",
clockTolerance: 5,
};
function getUser(req) {
const header = req.headers.authorization || "";
if (!header.startsWith("Bearer ")) {
throw new AuthError("Missing bearer token");
}
const claims = jwt.verify(header.slice(7), publicKey, VERIFY_OPTIONS);
if (revocationList.has(claims.jti)) {
throw new AuthError("Token revoked");
}
return { id: claims.sub, role: claims.role };
}The algorithm list is pinned in code, the issuer and audience are checked so a token from another service is useless here, and short lifetimes plus a jti revocation lookup limit how long a stolen token works. Keep signing keys in a KMS or secret manager, rotate them, and re-read the role from your own datastore for anything sensitive instead of trusting a claim that was minted minutes ago.