Fix AI Code

Why AI-Generated Authentication Always Breaks (And the Fix We Apply Every Time)

AI tools build a convincing login screen and almost never build a real security boundary. Here is why AI-generated auth fails the same way every time, and the server-side fix that closes the hole.

Michael Graham
Michael Graham
July 4, 2026· 5 min read

If we get an urgent message about a vibe-coded app, there is a better than even chance it is about authentication. Someone discovered that another user can see their data, or a security researcher emailed them, or a logged-out browser tab loaded a dashboard it had no business loading. The login looked finished. It was not. AI-generated authentication fails the same way nearly every time, and once you see the pattern you can check any AI-built app for it in about ten minutes.

The pattern: protecting the screen instead of the data

When you ask an AI tool to "add authentication" or "make this page require login," it builds the part it can see. It creates a login form, stores a session or a token, and conditionally renders the protected UI. Log out, and the dashboard disappears. It looks secure because the thing you were looking at went away.

The boundary it almost never builds is the one on the server. The API endpoint that feeds that dashboard still answers anyone who calls it directly. The component checked who you are. The endpoint did not.

// What the AI built: the check is in the UI
function Dashboard() {
  const { user } = useAuth()
  if (!user) return <Redirect to="/login" />
  return <OrdersList />   // calls GET /api/orders
}

// The endpoint behind it, as generated: no check at all
export async function GET() {
  const orders = await db.select().from(ordersTable)  // returns everyone's orders
  return Response.json(orders)
}

Anyone who opens the network tab, finds /api/orders, and requests it without logging in gets the data. The login screen never mattered.

The second failure: authenticated but not authorized

The apps that do check for a logged-in user often make the next mistake. They confirm you are someone, then hand back data without checking it is yours. This is the bug that lets user A read user B's records by changing an ID in the URL.

// Authenticated, but not authorized: any logged-in user can read any order
export async function GET(req) {
  const user = await requireUser(req)        // good: confirms a valid session
  const order = await db.query.orders.findFirst({
    where: eq(orders.id, req.params.id)        // bad: never checks order.user_id === user.id
  })
  return Response.json(order)
}

"Are you logged in" and "are you allowed to see this specific thing" are two different questions. AI-generated code routinely answers the first and skips the second.

Why every AI tool makes this mistake

This is not a flaw in one builder. It is structural. The model is trained to produce code that satisfies the visible request, and the visible request is a working login experience. UI state is observable and easy to demonstrate. Server-side authorization is invisible when everything goes right, so it is the part that gets dropped. The tool optimizes for the demo, and in the demo, the only user is you.

The fix: enforce identity and ownership on the server, every time

The durable fix is a single rule applied without exception: every endpoint that returns or changes user data verifies who is asking and that the data belongs to them, on the server, before it does anything.

export async function GET(req) {
  // 1. Identity: who is this, verified server-side?
  const user = await requireUser(req)

  // 2. Ownership: scope the query to this user, do not fetch then check
  const order = await db.query.orders.findFirst({
    where: and(
      eq(orders.id, req.params.id),
      eq(orders.user_id, user.id),
    ),
  })

  // 3. Fail closed: no row means not found, do not leak existence
  if (!order) return new Response('Not found', { status: 404 })

  return Response.json(order)
}

Three things make this work. Identity is checked on the server, not inferred from the UI. Ownership is enforced in the query itself, so you never fetch a record and then decide whether to return it. And the endpoint fails closed: when something is missing or wrong, the default is to deny.

To audit an existing app, list every API route, open the network tab, and call each protected route while logged out and then while logged in as a different user. Anything that returns data it should not is the hole. In a vibe-coded app you will usually find several, and they cluster exactly where you would expect: the endpoints that were added late, after the login screen was already "done."

Key takeaways

  • AI-generated auth protects the UI and leaves the API open. The login screen is not the boundary.
  • Authentication and authorization are different. Confirm who the user is, then confirm the data is theirs.
  • Scope ownership inside the query and fail closed, so a missing record denies by default instead of leaking.
  • Audit by calling every protected endpoint logged out and as a different user. The holes cluster around recently added routes.

This is the most common and the most dangerous failure in AI-built software, and it is also one of the most fixable once you know the shape of it. If you are taking an AI-built app to real users, fixing the auth boundary is step one. It is the same first move in our migration playbook and the fix behind the auth item in our Bolt.new troubleshooting guide.

Run the Proof

This is not just a description. It is a minimal, public repo that reproduces the bug above and proves the fix with passing tests. Clone it and run it yourself, or open it in the browser.

git clone https://github.com/commandcenterio/cmdcntr-demos.git
cd cmdcntr-demos/demos/ai-auth-boundary
pnpm install
pnpm test

See the bug fail

git checkout bug/ai-auth-boundary && pnpm test
ai codeauthenticationsecuritydebuggingproduction
Michael Graham
Michael Graham

Founder & Software Engineer

Obsessed with building top-tier web software and crafting unique, polished user experiences.