All articles

Clerk `auth().userId` Returns Truthy for Unauthenticated Users — Sometimes

Clerk `auth().userId` Returns Truthy for Unauthenticated Users — Sometimes

Clerk's App Router helpers return a userId that looks truthy even when the caller isn't authenticated. Here's the trap and the right guard.

June 4, 2026VibeWShield Team1 min read

The most common Clerk bug in vibe-coded Next.js apps:

import { auth } from "@clerk/nextjs/server";
 
export async function POST(req: Request) {
  const { userId } = auth();
  if (!userId) return new Response("unauthorized", { status: 401 });
  // ... trusted zone
}

Looks right. Isn't.

auth() returns an object with userId: string | null. When the user isn't signed in it's null, and the guard works.

But if the middleware didn't run (route not matched by config.matcher), auth() throws — or worse, returns cached data from a previous request in rare edge cases. Either way, the guard is no longer the sole authorization check.

The right pattern

Always verify the JWT explicitly on sensitive endpoints:

import { auth, currentUser } from "@clerk/nextjs/server";
 
export async function POST() {
  const { userId } = auth();
  if (!userId) return new Response("unauthorized", { status: 401 });
 
  const user = await currentUser();  // hits Clerk API — real proof
  if (!user) return new Response("unauthorized", { status: 401 });
 
  // Now trusted.
}

currentUser() is an async call that validates the session against Clerk's API. It costs ~50 ms but costs you zero CVEs.

Matcher pitfall

middleware.ts:

export const config = { matcher: ["/((?!_next|static).*)"] };

If your API route path isn't matched, the whole Clerk session handshake is skipped. Double-check with a literal curl to the endpoint — if it's 200 without a cookie, your matcher is wrong.

Free security scan

Test your app for these vulnerabilities

VibeWShield automatically scans for everything covered in this article and more — 18 security checks in under 3 minutes.

Scan your app free