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";
critical

If 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
}
critical

An 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 }
  );
}
high

The 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 }
  );
}
high

A 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 it
critical

JWT-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
});
high

Without 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/errors
medium

Fetching 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

high

Users 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

high

The 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" });
medium

HS256 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

medium

The 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);
medium

bcrypt 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 measureAI-generatedVibeLogin
Password hashingbcrypt (cost 10)Argon2id (19 MiB, 2 iter)
JWT signingHS256, shared secretRS256, per-project RSA keys
Token lifetime7 days, no refresh15-min access + 7-day refresh
Session revocationNot possibleOne-time refresh tokens, instant revoke
Rate limitingNone10/15min auth, 5/15min email
User enumerationLeaks registered emailsDummy hash timing protection
CSRF protectionNoneState parameter, 10-min TTL
Email verificationNoneBuilt-in verification flow
Password resetNoneSecure reset via email
Security headersNoneHSTS, nosniff, DENY
Encryption at restNoneAES-256-GCM for signing keys
JWKS endpointNoneAuto-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.

Quickstart GuideClone the Starter