Skip to content
Prompt of the Day — Part 5 of 30

Prompt of the Day: Create a Secure Authentication Flow with Clerk

Written by claude-sonnet-4 · Edited by claude-sonnet-4
clerkauthenticationnextjssecuritymiddlewareserver-actionsvibe-codingprompt-of-the-daytypescriptdefense-in-depth

Series: Prompt of the Day — Part 5 of 30 | Track: Prompts | By Tom Hundley


The Breach That Shouldn't Have Happened

In March 2025, Vercel disclosed CVE-2025-29927—a critical vulnerability (CVSS 9.1) affecting millions of Next.js applications. The exploit was embarrassingly simple: send a request with the internal header x-middleware-subrequest set to the right value, and Next.js would skip your middleware entirely. Authentication checks? Gone. Rate limiting? Skipped. Admin-only routes? Wide open.

The root cause wasn't a sophisticated attack. A developer—one Reddit commenter called it "vibe coding at its worst"—had added a depth check to prevent infinite middleware loops, and accidentally wired it so that exceeding five hops caused the middleware to bail out completely. Any attacker who knew the internal header name could hit /dashboard/admin without a valid session.

Apps that had auth only in middleware.ts were fully exposed. Apps that had redundant checks at the data layer? They survived.

That's the lesson we're baking into today's prompt.

November 2025 added a second wake-up call: Clerk's engineering team published a postmortem on a credential stuffing wave targeting their platform. Attackers were cycling millions of stolen credentials—drawn from a fresh dump of 625 million never-before-leaked passwords—using rotating IPs and TLS fingerprints to evade rate limiting. Even 99.9% effectiveness wasn't enough at that scale. Clerk responded with "Client Trust," a mechanism that treats every new device as untrusted and requires a second factor even when the password is correct.

The takeaway for your Next.js app: authentication isn't a single gate. It's a layered system. Today's prompt builds that system with Clerk.


The Prompt

Set up a complete, production-safe authentication flow for this Next.js 15 App Router project using Clerk. Do the following:

1. MIDDLEWARE LAYER
   - Create middleware.ts using clerkMiddleware() at the project root.
   - Use createRouteMatcher() to define public routes: ["/", "/sign-in(.*)", "/sign-up(.*)", "/api/webhooks(.*)"].
   - Redirect unauthenticated users hitting protected routes to /sign-in.
   - Do NOT rely on middleware as the sole security layer.

2. SERVER-SIDE PROTECTION
   - In every protected Server Component, call auth() from @clerk/nextjs/server and destructure userId.
   - If userId is null, call redirect("/sign-in") immediately. Do not conditionally render — redirect hard.
   - Create a reusable helper: requireAuth() in lib/auth.ts that wraps this pattern.

3. SERVER ACTIONS & API ROUTES
   - In every Server Action and Route Handler that mutates data, call requireAuth() at the top before any database access.
   - Add input validation using zod before processing any form data.

4. ENVIRONMENT & CONFIG
   - Show the required .env.local variables: NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, CLERK_SECRET_KEY, NEXT_PUBLIC_CLERK_SIGN_IN_URL, NEXT_PUBLIC_CLERK_SIGN_UP_URL, NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL.
   - Wrap the root layout with <ClerkProvider>.

5. UI COMPONENTS
   - Add <SignInButton />, <SignUpButton />, <UserButton />, and <SignedIn> / <SignedOut> conditional wrappers to the Navbar.

6. SECURITY NOTES IN CODE
   - Add a brief comment above each auth check explaining WHY it's there (e.g., // Defense-in-depth: middleware can be bypassed — verify session at data layer)

Output the complete file contents for: middleware.ts, lib/auth.ts, app/layout.tsx, components/Navbar.tsx, and one example protected page at app/dashboard/page.tsx.

Why It Works

This prompt succeeds because it encodes defense-in-depth as a structural requirement, not an afterthought.

1. Middleware + server-side redundancy. CVE-2025-29927 proved that middleware-only auth is a single point of failure. By requiring requireAuth() inside every Server Component and Server Action, you ensure that even if an attacker bypasses middleware with a spoofed header, they still hit a wall at the data layer. Clerk's own documentation describes this exact architecture: "Verify authentication at every data access point, not just middleware."

2. A reusable requireAuth() helper. Instead of copy-pasting const { userId } = auth(); if (!userId) redirect('/sign-in'); across 40 files, you centralize the pattern. When Clerk releases a new API or you need to add role-checking, you change one function.

3. Server Actions are public endpoints. This is the most commonly missed mistake in vibe-coded apps. A Server Action is just a POST endpoint. Anyone can call it directly. The prompt forces auth checks before any mutation happens—before any database write, before zod validation even runs.

4. Comment-driven security. The instruction to add a // Defense-in-depth comment isn't cosmetic. It's documentation that tells the next developer (and the next AI assistant) why the check exists, so it doesn't get "cleaned up" as apparent duplication.

5. Clerk's 60-second token architecture. Clerk implements 60-second JWT expiration with automatic background refresh at the 50-second mark. By using auth() server-side (which validates against Clerk's backend), you're getting a real-time session check, not a stale cookie read.


The Anti-Prompt

// ❌ DON'T DO THIS
Add authentication to my Next.js app using Clerk.
Protect all routes under /dashboard.

Why it fails:

This prompt is so open-ended that AI assistants will almost always produce a middleware-only solution. They'll write a clean middleware.ts, protect /dashboard(.*), and call it done. That output looks correct—it even passes basic testing. But it leaves you fully exposed to:

  • Middleware bypass attacks (see CVE-2025-29927 above)
  • Unprotected Server Actions—the AI won't know to add auth checks inside actions/createPost.ts unless you explicitly ask
  • Missing <ClerkProvider> in the root layout, causing runtime errors in production
  • No environment variable scaffolding, so the app silently breaks on first deploy

The output looks like working auth. It isn't. And since it does work locally during testing, you won't catch the gaps until something goes wrong in production.


Practical Code Example

Here's the requireAuth() helper the prompt generates—the cornerstone of your defense-in-depth strategy:

// lib/auth.ts
import { auth } from '@clerk/nextjs/server';
import { redirect } from 'next/navigation';

/**
 * Call at the top of any Server Component, Server Action, or Route Handler
 * that accesses protected data.
 *
 * Defense-in-depth: middleware can be bypassed (see CVE-2025-29927).
 * Always verify the session at the data layer.
 */
export async function requireAuth() {
  const { userId } = await auth();

  if (!userId) {
    redirect('/sign-in');
  }

  return { userId };
}

And a protected Server Action that uses it:

// app/actions/createPost.ts
'use server';

import { requireAuth } from '@/lib/auth';
import { z } from 'zod';

const PostSchema = z.object({
  title: z.string().min(1).max(200),
  body: z.string().min(10),
});

export async function createPost(formData: FormData) {
  // Defense-in-depth: Server Actions are public POST endpoints.
  // Always auth-check before touching the database.
  const { userId } = await requireAuth();

  const parsed = PostSchema.safeParse({
    title: formData.get('title'),
    body: formData.get('body'),
  });

  if (!parsed.success) {
    throw new Error('Invalid input');
  }

  await db.post.create({
    data: { ...parsed.data, authorId: userId },
  });
}

Variations

Role-based access control: Add role checking to requireAuth():

// lib/auth.ts (extended)
export async function requireRole(role: 'admin' | 'moderator') {
  const { userId, sessionClaims } = await auth();
  if (!userId) redirect('/sign-in');

  const userRole = sessionClaims?.metadata?.role;
  if (userRole !== role) redirect('/unauthorized');

  return { userId };
}

Webhook endpoint protection — Clerk webhooks use Svix signatures, not sessions. Tell your AI explicitly:

For the /api/webhooks/clerk route, do NOT use requireAuth().
Instead, verify the Svix webhook signature using the WEBHOOK_SECRET
environment variable and the svix npm package.

Organization-scoped auth — If your app is B2B:

Extend requireAuth() to also check that auth().orgId matches
the organization in the URL params. Redirect to /org-required
if the user is not in an active organization.

Enabling Client Trust (credential stuffing defense): No code change needed—Clerk's Client Trust is auto-enabled for new apps. For existing apps, enable it in your Clerk Dashboard under Security → Client Trust. It automatically requires a second factor for any new device, even when the password is correct, neutralizing credential stuffing attacks powered by leaked credential databases.


Checklist

Before you ship auth on your next project:

  • middleware.ts uses clerkMiddleware() with createRouteMatcher() for public routes
  • Every protected Server Component calls requireAuth() — not just relies on middleware
  • Every Server Action and Route Handler that mutates data calls requireAuth() at line 1
  • Zod validates all input in Server Actions before database access
  • .env.local has all five Clerk environment variables
  • Root layout.tsx is wrapped with <ClerkProvider>
  • Next.js version is 15.2.3 or later (patches CVE-2025-29927)
  • Client Trust is enabled in your Clerk Dashboard
  • MFA options (TOTP, backup codes) are configured for sensitive routes
  • Webhook endpoints use Svix signature verification, not requireAuth()

Ask The Guild

This week's community prompt:

We've covered the defense-in-depth pattern for Clerk + Next.js. But what's your biggest auth gotcha?

Share in the Discord #prompt-of-the-day channel:

What auth mistake have you caught in your own codebase (or an AI-generated one) that looked correct but wasn't? Middleware-only protection, leaked session data, unprotected Server Actions — what almost got you?

The best responses get featured in Part 6.


Tom Hundley is a software architect with 25 years of experience. He teaches vibe coders how to ship fast without sacrificing the foundations that keep production systems standing.

Copy A Prompt Next

Review and debug

If this article changed how you think about the problem, copy a prompt that turns that judgment into one safe, reviewable next step.

Matching public prompts

23

Keep the task scoped, copy the prompt, then inspect one reviewable diff before the agent continues.

Need the safest first move instead? Open the curated sample prompts before you browse the broader library.

Working With AI ToolsWorking With AI Tools

System Prompts — .cursorrules and CLAUDE.md Explained

Write system prompts that give AI persistent context about your project and preferences.

Preview
**Use this when you want the agent to draft your persistent project instructions:**
"Help me write a system prompt file for this project.
Tool target: [Cursor / Claude Code / both]
Project summary: [what the app does]
Stack: [frameworks, languages, key services]
Prompt Engineering

Turn this workflow advice into a durable operating system

Prompt and workflow posts are the quick win. The learning paths turn them into a durable operating model for tools, prompts, and agent supervision.

Best Next Path

Working With AI Tools

Explorer · Free

Turn ad hoc prompting into a repeatable workflow with better tool choice, stronger prompting, and safer day-to-day AI habits.

23 lessonsIncluded in the free Explorer plan

Need the free route first?

Start with Foundations for AI-Assisted Builders if you want the workflow and vocabulary before you dive into the deeper path above.

T

About Tom Hundley

Tom Hundley writes for builders who need stronger technical judgment around AI-assisted software work. The Guild turns production experience into public articles, copy-paste prompts, and structured learning paths that help non-software developers supervise AI agents more safely.

Do this next

Leave this article with one concrete move. Copy the matching prompt, or start with the path that teaches the safest next skill in sequence.