DEV Community

Cover image for 3 Broken Auth Patterns Cursor Keeps Writing Into Your API
Charles Kern
Charles Kern

Posted on

3 Broken Auth Patterns Cursor Keeps Writing Into Your API

TL;DR

  • AI tools write jwt.decode() instead of jwt.verify() -- zero signature validation
  • The middleware "works" in dev so it ships, and your API silently accepts forged tokens
  • Replace decode with verify, explicit algorithm allowlist, wrap in try/catch

Last month I was reviewing a side project a friend asked me to look at. Node/Express backend, JWT auth, standard stuff. First thing I checked was the auth middleware. There it was: jwt.decode(token). Not jwt.verify. Not even close.

I asked how they built it. "Cursor wrote most of it." That tracks. I've seen this exact pattern in at least six projects this past year -- all AI-assisted, all using decode instead of verify.

The scary part: jwt.decode works perfectly. Tests pass. App runs. No errors. You have no idea your API accepts any token you hand it, signed or not.

The Vulnerable Code (CWE-287)

// AI-generated auth middleware
const jwt = require('jsonwebtoken');

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });

  const decoded = jwt.decode(token); // CWE-287: no signature verification

  if (!decoded) return res.status(401).json({ error: 'Invalid token' });

  req.user = decoded;
  next();
}
Enter fullscreen mode Exit fullscreen mode

jwt.decode strips the base64 off the JWT payload and returns it as a plain object. That's it. No signature check. No expiry check. No validation that the token was signed with your secret. You could open jwt.io, swap the user ID to an admin account, and hit every protected endpoint in the app.

Two variants I've seen from Copilot and Claude Code respectively:

// Variant 1: algorithm confusion -- accepts alg:none
const decoded = jwt.verify(token, secret, { algorithms: ['HS256', 'none'] });

// Variant 2: ignoring expiry
const decoded = jwt.verify(token, secret, { ignoreExpiration: true });
Enter fullscreen mode Exit fullscreen mode

Both get generated when the developer asks the AI to "stop throwing errors on token verification." The AI fixes the immediate complaint and ships broken auth.

Why AI Keeps Doing This

The pattern comes from documentation examples. JWT library READMEs show jwt.decode as a quick preview -- "here's what a decoded token looks like" -- before getting into verification. Models trained on millions of repos see this everywhere and reproduce it, especially when the surrounding context looks like "just check if the user is logged in."

The ignoreExpiration variant comes from StackOverflow workarounds for expired tokens during local testing. The AI absorbs the fix without the context of why it was temporary.

The Fix

const jwt = require('jsonwebtoken');

function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return res.status(401).json({ error: 'No token' });

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET, {
      algorithms: ['HS256'] // explicit allowlist -- never include 'none'
    });
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(401).json({ error: 'Invalid token' });
  }
}
Enter fullscreen mode Exit fullscreen mode

Three differences from the broken version: verify not decode, explicit algorithm allowlist, and a try/catch that blocks the request on any verification failure. Secret comes from an env variable -- not hardcoded, not an empty string.

Quick check to see why the algorithm allowlist matters:

node -e "const j=require('jsonwebtoken'); console.log(j.decode('eyJhbGciOiJub25lIn0.eyJ1c2VySWQiOiIxMjMifQ.'))"
Enter fullscreen mode Exit fullscreen mode

That token has alg:none. Without an explicit allowlist, some configurations will accept it.

I've been running SafeWeave for this. It hooks into Cursor and Claude Code as an MCP server and flags these patterns before I move on. That said, even a basic pre-commit hook with semgrep and gitleaks will catch most of what's in this post. The important thing is catching it early, whatever tool you use.

Top comments (0)