Quick Answer
AI-generated JWT implementations often have three critical flaws: hardcoded secrets (often "secret" or "your-secret-key"), no algorithm restriction (which exposes you to the "none" algorithm and key confusion attacks in older or misconfigured libraries), and missing token expiry. A guessable secret alone lets attackers forge valid tokens and access any account in your application; the other two widen the damage.
Why AI Gets JWT Wrong
JSON Web Tokens (JWT) are a common authentication mechanism in AI-generated APIs. AI coding tools often generate JWT-based authentication when asked for user login. The problem is that working JWT code and secure JWT code are very different things.
JWT misconfiguration is a common authentication weakness in modern web applications. The OWASP Web Security Testing Guide has dedicated tests for the "none" algorithm, HMAC vs public key confusion, and weak HMAC keys. AI tools generate code that demonstrates these exact vulnerabilities.
PortSwigger's guide to JWT attacks notes that JWT vulnerabilities typically arise from flawed JWT handling within the application itself. AI-generated JWT code is especially prone to this, because the models learn from tutorial-quality examples, not production-hardened implementations.
Vulnerability 1: Hardcoded JWT Secrets
// ❌ BAD - AI's favorite JWT patterns
jwt.sign(payload, 'secret');
jwt.sign(payload, 'your-secret-key');
jwt.sign(payload, 'jwt-secret-123');
jwt.sign(payload, process.env.JWT_SECRET || 'fallback-secret');
// ✅ GOOD - Strong secret, no fallback
// Generate with: node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
const token = jwt.sign(payload, process.env.JWT_SECRET, {
algorithm: 'HS256',
expiresIn: '1h',
});
// JWT_SECRET must be at least 256 bits (64 hex chars) for HS256
Vulnerability 2: The "none" Algorithm Attack
The JWT specification includes an "alg": "none" option that creates unsigned tokens. If your JWT library or hand-written verifier does not restrict algorithms, an attacker can modify a token, set the algorithm to "none", remove the signature, and your server may accept it as valid. The popular jsonwebtoken package rejects unsigned tokens by default since version 9.0.0, but versions up to 8.5.1 accepted them when the secret was missing (CVE-2022-23540), and other libraries differ. Pin the algorithm anyway.
// ❌ BAD - No algorithm restriction
const decoded = jwt.verify(token, secret);
// Relies on library defaults; older versions accepted "none"
// ❌ BAD - Algorithm confusion: RS256 key used with HS256
const claims = jwt.verify(token, publicKey);
// A library that trusts the token's "alg" lets an attacker
// sign an HS256 token with the public key as the HMAC secret
// ✅ GOOD - Explicitly restrict algorithm
const decoded = jwt.verify(token, secret, {
algorithms: ['HS256'], // ONLY accept HS256
complete: true,
});
Vulnerability 3: Missing or Excessive Token Expiry
AI-generated code either sets no expiry at all (tokens valid forever) or sets extremely long expiry periods. A stolen token with no expiry gives permanent access to the victim's account.
| Token Type | AI Default | Secure Default | Why |
|---|---|---|---|
| Access token | No expiry or 30 days | 15-60 minutes | Limits damage window if stolen |
| Refresh token | Not implemented | 7-30 days, rotated on use | Allows session extension safely |
| Password reset | 24 hours or no expiry | 15-30 minutes | Minimizes attack window |
| Email verification | No expiry | 24 hours | Prevents stale verification links |
Complete Secure JWT Implementation
// ✅ GOOD - Production-ready JWT setup
import jwt from 'jsonwebtoken';
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET || JWT_SECRET.length < 64) {
throw new Error('JWT_SECRET must be set and at least 64 characters');
}
function generateTokens(userId) {
const accessToken = jwt.sign(
{ sub: userId, type: 'access' },
JWT_SECRET,
{ algorithm: 'HS256', expiresIn: '15m' }
);
const refreshToken = jwt.sign(
{ sub: userId, type: 'refresh' },
JWT_SECRET,
{ algorithm: 'HS256', expiresIn: '7d' }
);
return { accessToken, refreshToken };
}
function verifyAccessToken(token) {
const payload = jwt.verify(token, JWT_SECRET, { algorithms: ['HS256'] });
if (payload.type !== 'access') {
throw new Error('Not an access token');
}
return payload;
}
How to Audit Your JWT Implementation
- Search for hardcoded secrets: Grep for
jwt.signand check the secret parameter. If it is a string literal, replace with an env var. - Check algorithm restrictions: Every
jwt.verifycall must include{ algorithms: ['HS256'] }(or your chosen algorithm). - Verify token expiry: Every
jwt.signcall should includeexpiresIn. - Look for refresh token rotation: If your app uses refresh tokens, ensure they are rotated (old token invalidated) on each use.
- Run automated scanning: VibeDoctor's Vibe Check flags real-looking secrets assigned to variables like
JWT_SECRET, JWTs committed in source, and leaked keys. It does not inspectjwt.signorjwt.verifyoptions, and it skips obvious placeholders like'your-secret-key', so steps 1-4 still need a manual pass. Free to sign up.
If you would rather not hand-roll tokens at all, our comparison of Clerk, NextAuth and Supabase Auth looks at where AI-generated code breaks each one.
FAQ
Should I use JWT or session cookies for authentication?
For most web applications, session cookies with httpOnly and Secure flags are simpler and harder to get wrong. JWT is better for stateless APIs, microservices, and mobile app backends. If you are building a standard Next.js app, consider using NextAuth or Clerk instead of implementing JWT yourself.
What is algorithm confusion in JWT?
Algorithm confusion (or key confusion) happens when a server using RS256 (asymmetric) does not restrict the algorithm. An attacker can switch the algorithm to HS256 (symmetric) and sign the token with the public key, which the server then accepts. Always specify algorithms: ['RS256'] or algorithms: ['HS256'] in your verify call.
How long should my JWT secret be?
For HS256, the secret must be at least 256 bits (32 bytes, or 64 hex characters). Use crypto.randomBytes(64).toString('hex') to generate a secure secret. Never use dictionary words, short strings, or common phrases. The secret's entropy directly determines how resistant it is to brute-force attacks.
Can I invalidate a JWT before it expires?
Not without additional infrastructure. JWTs are stateless - once issued, they are valid until expiry. To enable early revocation, maintain a blocklist of revoked token IDs (the jti claim) in Redis or your database. This is why short access token expiry (15 minutes) paired with refresh tokens is the recommended pattern.