All posts

28 Oct 2026 · Auth & Stripe

Next.js 15 Server Actions Authorization, Stopping IDOR

Server Actions are POST endpoints. How to check the Auth.js session inside the action, reject cross-tenant ids, and share one guard across mutations.

A Server Action looks like a function. On the wire it is a POST to your origin with an id in the body. If you updateThread(threadId) without checking who owns threadId, any signed-in user can pass someone else's cuid. That is IDOR.

Auth.js v5 lives in AgenticPacks OSS: Agentic-Packs/agenticpacks-oss. Pro adds RBAC on team workspaces (Owner, Admin, Member). For the Clerk bill you are trying to avoid, see Auth.js v5 vs Clerk.

The action is public until you say otherwise

"use server" does not consult the session. middleware.ts that only protects /dashboard does not run for every action if the client calls it from a public layout. Treat each action as an API route you forgot to hide.

"use server";

import { auth } from "@/auth";
import { prisma } from "@/lib/prisma";

export async function renameThread(threadId: string, title: string) {
  const session = await auth();
  if (!session?.user?.id) {
    throw new Error("unauthorized");
  }

  const thread = await prisma.conversation.updateMany({
    where: { id: threadId, userId: session.user.id },
    data: { title },
  });

  if (thread.count !== 1) {
    throw new Error("forbidden");
  }
}

updateMany with userId in where is the cheap version. findFirst then update is clearer when you also check a team role.

Never trust userId from the client. The session is the only id that counts.

One wrapper, not 40 copy-pastes

import { auth } from "@/auth";

export async function requireUser() {
  const session = await auth();
  if (!session?.user?.id) {
    throw new Error("unauthorized");
  }
  return session.user;
}

export async function requireThreadOwner(threadId: string, userId: string) {
  const thread = await prisma.conversation.findFirst({
    where: { id: threadId, userId },
  });
  if (!thread) {
    throw new Error("forbidden");
  }
  return thread;
}

Every mutation starts with requireUser(). Every row mutation then scopes by userId or organizationId. Pro workspaces add requireMembership(orgId, ["OWNER", "ADMIN"]).

Throwing is fine for actions. The client should already hide the button. The server still decides.

What middleware cannot save

Middleware can redirect anonymous users from /dashboard. It cannot see that threadId belongs to another tenant. IDOR is a data filter, not a cookie check.

Rate-limit expensive actions (embed, ingest, agent run) after auth. A valid member can still burn tokens. The team credit pool post covers deduct-then-run.

Auth.js v5 session in Node

Use the Node runtime for actions that hit Prisma. The Auth.js session strategy on a VPS is JWT or a database session in Postgres. Do not put the guard in Edge-only middleware and assume the action inherited it.

Cookie config must be secure in production behind Caddy. That TLS path is Pro. AgenticPacks OSS still needs AUTH_SECRET at runtime. See Docker env vars.

Tired of bolting session checks onto every action?

AgenticPacks OSS already has Auth.js v5 and Prisma user ids: Agentic-Packs/agenticpacks-oss.

Pro adds team RBAC on the same pattern. See workspace RBAC in Pro.

Checklist

CheckFail mode
auth() is nullAny visitor POSTs the action
where lacks userIdSigned-in user reads another inbox
Client sends userIdAttacker swaps the id
No org role checkMember deletes the workspace
No credit debitValid user runs up the bill

Look at your update and delete actions first. Those are the IDORs that ship.


Where to Go From Here

You do not have to invent a policy framework to make Server Actions safe.

Option A: Free open-source baseline (MIT)


Option B: Ship with AgenticPacks Pro ($149 one-time)

If the product is teams, not a single user:

  • Automated Hetzner provisioning: Terraform, Caddy, Compose.
  • Production multi-agent core: LangGraph, pgvector, Redis, workspace-scoped threads.
  • Usage & Unit Economics: Stripe, RBAC (Owner, Admin, Member), team credit pools.