Auth inside every action
A Server Action is a public endpoint. Check auth inside each one.
Next.js Docsnextjs.org
Treat Server Actions with the same security considerations as public-facing API endpoints, and verify if the user is allowed to perform a mutation.
From vercel-react-best-practices, rule server-auth-actions.
Page guarded, action exposed
As Alice, send the POST anyway. Then sign out.
No check insideBad
await db.user.delete({ where: { id: userId } })verifySession() insideGood
verifySession() → role check → delete'use server'
import { verifySession } from '@/lib/auth'
import { unauthorized } from '@/lib/errors'
export async function deleteUser(userId: string) {
const session = await verifySession()
if (!session) throw unauthorized('Must be logged in')
if (session.user.role !== 'admin' && session.user.id !== userId) {
throw unauthorized('Cannot delete other users')
}
await db.user.delete({ where: { id: userId } })
return { success: true }
}
Who can delete whom
Pick an implementation. Click a cell to trace it.
Caller × targetPolicy: admin, or yourself
| Caller | Alice's id | Bob's id |
|---|
TracePick a cell
Validate, auth, then mutate
Pick a preset or edit the form, then run.
Straight to the DBBad
db.user.update({ where: { id: data.userId }, data })Four gates, in orderGood
parse → verifySession → owner check → update'use server'
import { verifySession } from '@/lib/auth'
import { z } from 'zod'
const updateProfileSchema = z.object({
userId: z.string().uuid(),
name: z.string().min(1).max(100),
email: z.string().email()
})
export async function updateProfile(data: unknown) {
const validated = updateProfileSchema.parse(data) // 1. validate
const session = await verifySession() // 2. authenticate
if (!session) throw new Error('Unauthorized')
if (session.user.id !== validated.userId) { // 3. authorize
throw new Error('Can only update own profile')
}
await db.user.update({ // 4. mutate
where: { id: validated.userId },
data: { name: validated.name, email: validated.email }
})
return { success: true }
}
Why it matters, pros and cons
Why it matters
Anyone can POST to an action with any arguments. Only an inside check always runs.
Pros
- Holds against scripts and replayed requests.
- Auth sits next to the mutation it guards.
- Bad input is rejected before any lookup.
Cons
- One session lookup per call; cache with
React.cache(). - Repeated boilerplate; wrap it in one helper.
- Public actions skip auth but still need validation.