On this page
- 1. Passwords stored reversibly, or compared insecurely
- 2. Tokens decoded but never verified
- 3. Session tokens with no expiry
- 4. Session state that logout doesn’t actually invalidate
- 5. No session invalidation after a credential change
- 6. Cookie sessions with no CSRF protection
- 7. Unlimited login, reset, and MFA attempts
- 8. Password reset links that leak or never expire
- 9. Account enumeration in login and registration
- 10. MFA that can be bypassed or quietly downgraded
- 11. Authenticated but not authorized
- 12. Machines authenticating with borrowed user credentials
- Checklist
- FAQ
- Related topics
- Sources
The 12 Authentication Mistakes That Show Up in AI-Generated Apps
Reversible password storage, unverified tokens, sessions that never die, enumerating logins, bypassable MFA, and unprotected endpoints — twelve auth defects generated code ships with, and the fix for each.
Quick answer
- These twelve recur in generated apps because each one passes a demo, a unit test, and a code review that’s looking at structure rather than attack paths.
- The most damaging are silent: a token whose signature is never verified, and an endpoint that authenticates the caller but never checks ownership.
- Five are about how credentials are stored, transported, and invalidated. Four are about flows that leak information. Three are about trusting something you should have verified.
- The fix for most items is fewer lines, not more — remove the bypass, verify the token, expire the session.
- The fifteen security failures pillar lists these at summary level; this page is the authentication-specific depth, with the failure mechanics and the fix for each.
1. Passwords stored reversibly, or compared insecurely
The user table has a password column holding ciphertext, a base64 blob, or a raw string, and login compares the submitted value with ==. Encryption is reversible by design, so anyone with the key — including anyone who reaches your application’s environment — recovers every password.
Generated code does this because “encrypt the password” is the phrase in the prompt, and encryption is what the word means. The distinction between reversible encryption and one-way hashing is the actual lesson, and it’s the difference between a database leak and a credential leak that compromises your users on other sites too.
The fix: hash with a deliberately slow, salted algorithm — Argon2id, scrypt, or bcrypt — and compare with the library’s constant-time verification function, never ==. See How to Store Passwords Correctly (Hashing vs Encryption).
2. Tokens decoded but never verified
The middleware calls the JWT library’s decode function and trusts the resulting claims: user_id, role, tenant_id. Decoding parses base64; it does not prove the token was issued by you. An attacker crafts a payload with role: "admin", signs it with nothing or with a key they control, and the app accepts it.
This is the highest-severity item on the list, and the hardest to spot in review because the decoded object looks exactly like a legitimate one in every log line and test assertion. Verification is an explicit parameter or a separate call, and it’s easy to omit when only the happy path is exercised.
The fix: verify the signature against your key, and check the issuer, audience, and expiry, before reading any claim. Reject alg: none. Keep the signing key in the environment or a secret manager, never in source. See JWT Security: Common Mistakes That Get Tokens Stolen and How to Manage Secrets and Environment Variables Properly.
3. Session tokens with no expiry
A token or session ID that never expires, or expires in a year, turns any single exposure into permanent access. It survives the password change that was supposed to evict the attacker, and it will still work after the user stops using the account.
Long expiries exist because they’re convenient — nobody gets logged out mid-task — and the fix is a refresh mechanism rather than a long-lived credential: short access tokens, a refresh path you actually implemented.
The fix: give access credentials a short lifetime, expire idle sessions, and set an absolute maximum regardless of activity. See How to Choose an Authentication Method for Your App: A Decision Guide.
4. Session state that logout doesn’t actually invalidate
Logout deletes the cookie in the browser and nothing else. The session row is still valid, the token is still signed, and anyone who captured it earlier keeps using it — including the attacker you were logging out because of. The user believes they’re safe, which makes this worse than having no logout button.
The cause is a stateless-token design adopted without a revocation story, and it’s the tradeoff that makes JWTs the wrong default for a single web app.
The fix: for server-side sessions, delete the record on logout. For stateless tokens, keep a revocation list or a token version on the user row that you check, and bump it on logout. See Authentication and Authorization for AI Builders.
5. No session invalidation after a credential change
The user changes their password or email address after suspecting compromise, and every existing session stays valid. Password change is the moment the account owner expects to evict an intruder, and in generated code it usually updates one column and returns.
The fix: on password change, email change, and MFA enrollment, invalidate all other sessions — delete server-side records or bump the user’s token version — and notify the account owner. Test it: change the password in one browser and confirm the other is signed out. See How to Test Your App for Broken Access Control for the two-account habit that catches this class.
6. Cookie sessions with no CSRF protection
If authentication is a cookie, the browser attaches it automatically to requests from any site — that’s the feature that makes CSRF possible. A state-changing endpoint with no token check and no SameSite attribute can be triggered by a form on an attacker’s page, and the user’s browser signs the request for them.
Generated code often sets a session cookie with default attributes, which in older browsers means no protection at all, and the risk concentrates on POST, PUT, and DELETE endpoints that change data.
The fix: set SameSite=Lax or Strict, use Secure and HttpOnly, and require a CSRF token on state-changing requests. Never let a GET request mutate data. See What Is CSRF (Cross-Site Request Forgery)?.
7. Unlimited login, reset, and MFA attempts
Nothing throttles the endpoints that accept guesses. Login, password reset, MFA verification, and any endpoint that takes a short code can be tried at machine speed, which turns a large credential list from someone else’s breach into a successful login on your app.
This is the class of attack that legitimate-looking traffic uses, so it leaves no error in your logs — just a large number of failures followed by a success, and by then the account is already accessed.
The fix: rate limit per IP and per account, add progressive delays or lockouts, and check submitted passwords against known-breach lists at registration. See How to Prevent Credential Stuffing Attacks and How to Add Rate Limiting to an API.
8. Password reset links that leak or never expire
Reset tokens that are long-lived, reusable, guessable, or delivered in a redirect parameter. A reusable token means anyone who sees the email once — through a shared inbox, a mail scanner, or browser history — can reset the account later, indefinitely.
Generated reset flows often generate a token with a weak random source, send it as a query parameter, and never invalidate it after use. Each of those is individually small and together they make the reset flow the weakest link in the account.
The fix: generate tokens from a cryptographically secure source, store only a hash of the token, expire them in minutes, and invalidate on first use or on any password change. See What Is an Open Redirect (and Why Do Phishers Love It)? for the redirect parameter variant.
9. Account enumeration in login and registration
The login form says “no account with that email” versus “incorrect password,” registration says “that email is taken,” and reset says “we sent a link” only for addresses that exist. Each one confirms to an attacker whether an address is registered, and the difference in response time between the two paths leaks it even when the messages are identical.
The information seems harmless and is genuinely useful for user experience, which is why it survives review. It’s the input to targeted phishing and credential stuffing campaigns — an attacker’s list gets much shorter and more valuable.
The fix: return one neutral message for all failure paths, make the timings comparable, and rate limit the endpoints regardless. See What Is Credential Stuffing (and How Does It Get Your Accounts)?.
10. MFA that can be bypassed or quietly downgraded
Second factors that exist but don’t hold: enrollment required for some users and not others, an API endpoint that authenticates without checking the MFA step, a “remember this device” cookie that never expires, a fallback to email when the authenticator fails, and SMS as the only option.
The common thread is that MFA is implemented in the login UI rather than enforced at the session layer — so any path that doesn’t go through that form is single-factor, and the attacker uses that one.
The fix: record MFA completion as a property of the session, require it before any privileged action, and make the level of assurance a decision the authorization layer can see. Prefer an authenticator app or a passkey over SMS. See What Is Phishing-Resistant MFA?.
11. Authenticated but not authorized
The endpoint verifies a valid session and then fetches the record by the ID in the URL. Any logged-in user can read, edit, or delete any record by changing that number, and the code contains a real permission check — the wrong one.
This is the highest-impact defect in generated applications and the one most likely to be a regulatory incident, because it’s customer data exposure rather than a nuisance. It survives review because the feature genuinely works for the user who built it.
The fix: put the ownership condition in the query — WHERE id = $1 AND owner_id = $2 — for reads, updates, and deletes, and return 404 when the row isn’t theirs. See How to Design Authorization for Your App and What Is Broken Access Control (IDOR)?.
12. Machines authenticating with borrowed user credentials
A scheduled job or integration that signs in as a user, or a single API key with full access shared across every consumer. The credential has no owner, no scope, and no expiry, so a leak grants everything and revocation breaks something unrelated.
Generated code does this because the shortest path to “the job needs to read orders” is to reuse the credential that can already read orders — a user’s.
The fix: issue a scoped credential per consumer, treat it as a non-human identity with an owner and a rotation schedule, and store it outside the repository. See What Is a Non-Human Identity (NHI)? and How to Automate API Key Rotation.
Where this bites vibecoders
Every item above is invisible to a demo. The app signs in, the session persists, the pages load, the tests pass — because each defect only manifests under conditions the assistant never simulates: an attacker willing to edit a token, a second account hitting your endpoint, a credential list from another breach, a browser that attaches a cookie to a request the user never made. The practical consequence is that reviewing authentication code for structure and naming finds nothing, because nothing looks wrong. Reviewing it means asking four questions per flow: what proves this claim is genuine, how long does this access last, what happens when it’s replayed by someone else, and what does the failure message tell an attacker. Those four questions cover all twelve items.
Checklist
- Hash passwords with Argon2id, scrypt, or bcrypt — never encrypt them.
- Verify token signatures, issuer, audience, and expiry before reading any claim.
- Give every credential a short lifetime and expire idle sessions.
- Delete the server-side session on logout, and on password or email change.
- Set
SameSite,Secure, andHttpOnlyon session cookies, and use CSRF tokens. - Rate limit login, reset, and MFA endpoints per IP and per account.
- Expire reset tokens in minutes and invalidate them on first use.
- Return one neutral message for every authentication failure.
- Enforce MFA at the session layer so no path can skip it.
- Put the ownership predicate in the query, not in a handler.
- Give every machine caller its own scoped, rotatable credential.
- Run the two-account test against every user-specific endpoint before launch.
FAQ
What is the most common authentication mistake in AI-generated code?
Decoding a token and trusting its contents without verifying the signature, issuer, and audience. The code reads as correct because the payload it prints looks right, and every test passes. Verification is a single explicit call that the assistant omits unless asked, and without it anyone can mint a token with any user ID in it.
Is hashing a password with SHA-256 good enough?
No. SHA-256 is designed to be fast, which is exactly what an attacker needs: commodity hardware tries billions of guesses per second against a fast hash. Password storage needs a deliberately slow, salted algorithm — Argon2id, scrypt, or bcrypt — with a work factor you can tune as hardware improves.
Is SameSite=Lax enough to prevent CSRF?
It covers the common case — a cross-site form post won’t carry the cookie — but it isn’t a complete defense. Cookies set without SameSite at all, state-changing GET requests, subdomain attackers, and browsers with differing behavior all leave gaps. Use SameSite plus a CSRF token on state-changing requests, and never let a GET mutate data.
How do I check my authentication is safe before launching?
Work through the twelve items above, then run one test that catches the rest: create two accounts, and request the first account’s resources using the second account’s session for every endpoint. Anything that returns data is a finding. That single pass finds more real problems than any tool, because it tests the endpoint someone forgot rather than the ones you remembered.
Related topics
- Authentication and Authorization for AI Builders
- How to Design Authorization for Your App
- How to Choose an Authentication Method for Your App: A Decision Guide
- What Are the Different Types of Authentication?
- JWT Security: Common Mistakes That Get Tokens Stolen
- How to Store Passwords Correctly (Hashing vs Encryption)