Security
AI-Generated Auth Is Broken
We asked AI coding tools to “add authentication to my Next.js app.” The generated code had 12 security vulnerabilities. Here is every one of them.
If you are using AI coding tools to build your app, there is a good chance your auth code looks like this. We generated authentication using the exact prompt a typical developer would use, then audited every line.
The code works. It compiles. Users can sign up and log in. But it is not production-ready, and shipping it means shipping 12 security vulnerabilities to your users.
The prompt
"Add authentication to my Next.js app."Every major AI coding tool generates essentially the same pattern: a users table, bcrypt hashing, JWT tokens, and cookie-based sessions. Here is the code they produce and what is wrong with it.
1Hardcoded JWT secret with fallback
const JWT_SECRET = process.env.JWT_SECRET || "your-secret-key";criticalIf JWT_SECRET is not set in your environment (which it often is not in development, and sometimes not in production), the fallback "your-secret-key" is used. Anyone who can read your source code (or guess this common default) can forge valid tokens and log in as any user.
The fix: Use asymmetric keys (RS256) so the signing key never leaves the server, and fail loudly if secrets are missing.
2No rate limiting on login
// No rate limiting — unlimited login attempts
export async function POST(request: NextRequest) {
const { email, password } = await request.json();
// ...check password and return token
}criticalAn attacker can send unlimited login requests. With no rate limiting, credential-stuffing attacks using leaked password databases are trivial — common passwords like 123456 or password are tested in seconds.
The fix: Rate limit to 10 attempts per 15 minutes per IP/email. Lock accounts after repeated failures.
3User enumeration on signup
if (existingUser.rows.length > 0) {
return NextResponse.json(
{ error: "User already exists" },
{ status: 400 }
);
}highThe response tells an attacker exactly which email addresses have accounts. They can scan your entire user base by trying signups with different emails and seeing which return “User already exists.”
The fix: Return the same response whether the email exists or not. Send a verification email in both cases.
4Weak password policy
if (password.length < 6) {
return NextResponse.json(
{ error: "Password must be at least 6 characters" },
{ status: 400 }
);
}highA 6-character minimum with no complexity requirements allows passwords like 123456, aaaaaa, and qwerty. Combined with no rate limiting, this is an open door.
The fix: Minimum 8 characters. Check against common password lists. Require mixed case or special characters.
5Tokens cannot be revoked
const token = signToken({ userId: user.id, email: user.email });
// This token is valid for 7 days — no way to invalidate itcriticalJWT-only sessions with no server-side store means you cannot log a user out. If a token is stolen, it is valid for 7 days. If a user changes their password, old tokens still work. If an admin bans a user, they can still access the app until the token expires.
The fix: Short-lived access tokens (15 minutes) with server-side refresh tokens that can be revoked.
6No CSRF protection
response.cookies.set("token", token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 24 * 7,
path: "/",
// Missing: sameSite: "strict" or CSRF token
});highWithout an explicit sameSite attribute or CSRF token, you are relying on browser defaults. Modern browsers default to lax, which blocks cross-origin POST but still allows top-level GET navigations with cookies — leaving state-changing GET endpoints and redirect-based flows vulnerable.
The fix: Set sameSite: "lax" explicitly and use state parameters for redirect-based flows.
7SELECT * exposes password hashes in memory
const result = await pool.query(
"SELECT * FROM users WHERE email = $1",
[email]
);
const user = result.rows[0];
// user.password (the hash) is now in memory and could leak via logs/errorsmediumFetching all columns means the password hash sits in application memory and could appear in error logs, crash dumps, or serialized responses.
The fix: Select only the columns you need: SELECT id, email, name, password FROM users. Clear the hash from memory after comparison.
8No email verification
highUsers can sign up with any email address — including someone else's. No verification step means fake accounts, spam, and no way to do password resets securely.
The fix: Send a verification email with a one-time token. Restrict access until the email is confirmed.
9No password reset flow
highThe generated code has no way for users to reset a forgotten password. This is not just a UX issue — it means users will reuse passwords from other sites, because they cannot recover their account if they forget.
The fix: Time-limited, single-use reset tokens sent via email. Invalidate all existing sessions on password change.
10HS256 symmetric signing
jwt.sign(payload, JWT_SECRET, { expiresIn: "7d" });mediumHS256 uses the same secret to sign and verify. If any service that verifies tokens is compromised, the attacker can forge new tokens. With RS256 (asymmetric), only the auth server has the private key — other services verify with the public key and cannot forge tokens.
The fix: Use RS256 with a per-project RSA key pair. Publish the public key via a JWKS endpoint.
11No security headers
mediumThe generated code sets no security headers. No Strict-Transport-Security, no X-Content-Type-Options, no X-Frame-Options. These are basic protections against downgrade attacks, MIME sniffing, and clickjacking.
The fix: Set HSTS, X-Content-Type-Options: nosniff, X-Frame-Options: DENY on all responses.
12bcrypt instead of Argon2
const hashedPassword = await bcrypt.hash(password, 10);mediumbcrypt is not broken, but it is showing its age. It caps input at 72 bytes, has no memory-hardness, and is increasingly vulnerable to GPU and ASIC attacks. Argon2id (the current OWASP recommendation) is memory-hard, resists side-channel attacks, and lets you tune time and memory independently.
The fix: Use Argon2id with at least 19 MiB memory and 2 iterations. Rehash existing bcrypt passwords on next login.
The real problem: you don't know what to ask
Even if AI models improve and learn to generate rate limiting or use Argon2, the fundamental problem remains: most developers don't know what they should be asking for.
When you type “add authentication to my app,” you get exactly what you asked for — signup, login, and a session cookie. You don't get rate limiting because you didn't ask for it. You don't get email verification because you didn't know you needed it. You don't get CSRF protection because you've never heard of it.
It goes beyond security. Most builders don't even know which routes in their app need to be protected. Which pages require a logged-in user? Which API endpoints should reject unauthenticated requests? What happens when a token expires mid-session? These are not things a typical developer thinks about when they type “add auth.”
Authentication is not a feature you bolt on. It is a security system with dozens of interconnected requirements — token rotation, key management, session revocation, abuse prevention, email flows, route protection — that no single prompt can capture. You shouldn't have to be a security expert to ship secure auth.
AI-generated auth vs. VibeLogin
| Security measure | AI-generated | VibeLogin |
|---|---|---|
| Password hashing | bcrypt (cost 10) | Argon2id (19 MiB, 2 iter) |
| JWT signing | HS256, shared secret | RS256, per-project RSA keys |
| Token lifetime | 7 days, no refresh | 15-min access + 7-day refresh |
| Session revocation | Not possible | One-time refresh tokens, instant revoke |
| Rate limiting | None | 10/15min auth, 5/15min email |
| User enumeration | Leaks registered emails | Dummy hash timing protection |
| CSRF protection | None | State parameter, 10-min TTL |
| Email verification | None | Built-in verification flow |
| Password reset | None | Secure reset via email |
| Security headers | None | HSTS, nosniff, DENY |
| Encryption at rest | None | AES-256-GCM for signing keys |
| JWKS endpoint | None | Auto-published, 1-hour cache |
Replace your AI-generated auth in 3 files
Delete your signup route, login route, middleware, and users table. Replace them with VibeLogin and get all 12 fixes for free.