There's a pattern I keep seeing. Someone builds a full stack app in a weekend, Cursor or Copilot fills in the auth boilerplate, it looks fine, tests pass, ships to prod. Then three months later, data breach, support tickets, or worse, silence while someone quietly owns the system.

This isn't about skill. Smart people with solid fundamentals fall into these traps too, especially when moving fast. But vibe coding specifically has a way of making these mistakes invisible until they're very visible.

Here's the full breakdown. Not just what's wrong, but why the AI keeps generating it wrong and what you should actually do.

1. Storing JWTs in localStorage

This one is everywhere. Every tutorial does it, so the AI learned from every tutorial.

localStorage is accessible to any JavaScript running on your page. You get an XSS vulnerability anywhere, a third-party script, a malicious npm package, a DOM injection, and every token on that page is immediately readable. No extra steps needed.

Use `httpOnly` cookies. The browser handles them, JavaScript can't read them, and you get CSRF protection on top with `SameSite=Strict`. Yes it's slightly more work to set up. Worth it every single time.

2. JWT Signed With "secret" or "your_jwt_secret_here"

If your secret came from a tutorial, it's already in attacker wordlists. Literally. People maintain lists of common JWT secrets scraped from public repos and documentation.

The AI will copy whatever was in the training data. And a lot of training data has `JWT_SECRET=secret` in `.env.example` files.

Generate a proper 256-bit random secret. One command: `openssl rand -base64 32`. Store it in your secrets manager, not in your codebase.

3. No Refresh Token Rotation

If a refresh token gets stolen, through a log leak, a compromised device, a MITM on an insecure connection, without rotation, that token works indefinitely. The attacker has permanent access and you'd never know.

Rotation means: every time a refresh token is used, you issue a new one and invalidate the old one. If the old one gets used again, that's a red flag, someone's replaying a stolen token, and you kill the session immediately.

Most auth libraries have this as a one-line config. There's no good reason to skip it.

4. No Account Lockout After Failed Logins

Without lockout, a brute force attack is just a for loop. With enough time and a GPU, weak passwords fall fast.

10 failed attempts should trigger a temporary lockout. Add exponential backoff so each subsequent failure waits longer. Add CAPTCHA if you want to be more aggressive. Log the attempts. Alert on unusual patterns.

The AI will generate a login endpoint that just returns "wrong password" forever. That's not a feature, that's an invitation.

5. Auth Middleware Applied to Some Routes and Not Others

This is the one I see cause the most actual damage.

AI generates middleware for the routes it's asked about. If you ask for "a protected dashboard route," it protects that route. The `/api/admin/users` route you added later? Not protected. The `/api/export/all-data` endpoint? Also probably not protected.

Every endpoint that touches user data or performs state changes needs auth middleware. The only way to verify this is to audit manually. Go through every route in your codebase, check the middleware stack, assume nothing is protected by default. This is a 30-minute job that prevents a very bad day.

6. Different Error Messages for Wrong Email vs Wrong Password

"User not found" vs "Incorrect password" seems helpful. What you're actually doing is building a free account enumeration API for attackers.

With different messages, someone can check whether any email has an account on your platform without logging in. That's useful for targeted phishing, credential stuffing, and harassment.

Return the same message for both cases: "Invalid email or password." Don't confirm whether the account exists. The slight UX inconvenience is not worth the security hole.

7. Password Reset Tokens That Never Expire

A reset link from 6 months ago sitting in someone's old email inbox, if your tokens don't expire, that link still works. Forgotten email accounts, breached inboxes, screenshots in old chats, all of these are real attack surfaces.

Set expiry to 15–60 minutes max. Single use only, invalidate the token as soon as it's consumed. This should be the default. It almost never is out of the box with AI-generated code.

8. OAuth redirect_uri Not Validated

If you're not explicitly whitelisting valid redirect URIs in your OAuth flow, an attacker can craft an authorization request that redirects the auth code to their server. From there, they exchange it for a token and they're in.

Whitelist every valid redirect URI explicitly in your OAuth provider config. No wildcards, no partial matches, no "startsWith" checks. Exact string match. And never allow open redirects anywhere in your auth flow, not just in OAuth, period.

9. No Email Verification on Signup

Without verification, anyone can sign up with any email address. This opens you to fake accounts, spam, account takeover setups (register with someone's email before they do), and reputation issues with email providers.

Verify before granting full access. A short lived verification link, not just a welcome email. Until verified, limit what the account can do. This also helps you build a clean, deliverable email list as a side effect.

10. Sessions Not Invalidated Server-Side on Logout

Clearing the cookie client-side is not logout. The session record still exists on the server. Anyone with the old session ID, from a stolen cookie, a shared device, a cached browser, can still authenticate.

When a user logs out, invalidate the session record in your database or session store. If you're using JWTs without a server side session, you need a token blocklist. Yes, this adds complexity. It's the only way to make logout actually mean logout.

11. Passwords Stored Without Proper Hashing

MD5, SHA-256 without a salt, plain text, these are not theoretical risks. They show up in breach headlines constantly.

MD5 is broken. SHA-256 without a salt is vulnerable to rainbow table attacks. Neither is designed for password storage.

Use bcrypt or Argon2. Both are purpose built for passwords, they're deliberately slow, they handle salting automatically, and they have cost factors you can tune up as hardware gets faster. No exceptions here.

12. Auth Endpoints Not Enforcing HTTPS

HTTP sends credentials in plaintext. Anyone on the same network, coffee shop WiFi, a hotel router, a corporate proxy, can see them. This is basic but AI generated infrastructure configs frequently leave HTTP open as a fallback "for development."

There is no valid reason for an auth endpoint to accept HTTP in production. Enforce HTTPS at the infrastructure level. Redirect HTTP to HTTPS. No HTTP fallback for anything touching credentials.

13. Client-Side Role Checks Instead of Server-Side

This is the one that confuses newer developers the most.

You cannot trust anything the frontend sends you about who the user is or what role they have. None of it. A user can modify localStorage, flip flags in the browser, intercept and modify API requests. If your authorization logic lives in the frontend, it's not authorization, it's a suggestion.

Validate roles and permissions on every single server request. The frontend decides what to show. The server decides what to allow. These are different jobs.

14. No 2FA on Admin or Sensitive Routes

One leaked password = full admin access. This happens constantly. Credential stuffing, phishing, breach databases, passwords get out. 2FA means a leaked password alone isn't enough.

TOTP (Google Authenticator, Authy) is the minimum for admin routes. For very sensitive operations, consider step-up authentication, re-verify identity at the point of the sensitive action, not just at login.

If your app handles user data in any meaningful way, this is non-negotiable.

15. Test Credentials Left in Production

`admin:admin`, `test@test.com:password123`, `demo:demo`, these aren't convenience, they're open doors. They're in every public wordlist. Attackers try them on every app they scan.

Audit before every production deployment. Search your codebase for hardcoded credentials, seed files with test users, and default admin setups. Remove them. Automate a check in your CI pipeline if you can.

16. No Rate Limiting on Auth Endpoints

This one gets missed more than you'd think. You can have lockout logic, but if there's no rate limiting at the network level, an attacker can distribute the attack across IPs and bypass per-account lockout entirely.

Rate limit login, signup, password reset, and OTP verification endpoints. Not just by IP, also by endpoint globally. Cloudflare, nginx, your API gateway, wherever it makes sense in your stack. Auth endpoints should not be hammerable.

17. Logging Sensitive Auth Data

The AI will generate logging code that logs request bodies for debugging. That's fine for most routes. It is very not fine for auth routes.

If you log request bodies on login endpoints, you're logging passwords. If you log JWT payloads, you're logging session tokens. These logs end up in your logging service, your S3 bucket, your monitoring dashboard, places with different access controls than your auth system.

Never log passwords, tokens, reset codes, or OTPs. Audit your logging config specifically for auth routes. This one is sneaky because it's silent, you won't know until someone has your logs.

18. CORS Misconfigured to Allow Any Origin on Auth Endpoints

`Access-Control-Allow-Origin: *` on an auth endpoint is essentially saying any website can make credentialed cross-origin requests to your auth API. Combined with poor CSRF handling, this can be exploited to perform actions on behalf of authenticated users from attacker-controlled pages.

Be explicit about allowed origins. Never use wildcard for endpoints that handle credentials or session data. If your API is consumed by multiple frontends, whitelist each one explicitly.

Why Vibe Coding Makes This Worse

Here's the honest thing to understand: AI code generators are trained on the internet. The internet is full of tutorial code, demo repos, and "get it working fast" examples, none of which were written with production security in mind.

When you ask an AI to "add authentication," it's pattern matching against that training data. It generates what authentication code usually looks like, which is usually insecure by default, optimized for clarity and brevity, not for threat resistance.

This isn't a reason to not use AI for coding. It's a reason to treat AI-generated auth code with more suspicion than you treat any other code it writes. Auth is the one area where the defaults are almost always wrong.

The fix isn't to write everything from scratch. It's to:

  1. Use a battle-tested auth library (Auth.js, Clerk, Supabase Auth, Ory) wherever possible instead of rolling your own
  2. 2. Treat every AI-generated auth route as untrusted until you've manually reviewed it against this list

3. Run a security audit, even a quick one, before every production deploy

Ship fast. But ship this one carefully.

Found something missing from the list? Most auth bugs are silent until they're not. Audit your current project against these points. Guarantee at least 3 of them apply.