DEV Community

Cover image for Next.js 16 Server Actions Security: The Auth Check Most Developers Miss

Next.js 16 Server Actions Security: The Auth Check Most Developers Miss

Shubhra Pokhariya on June 24, 2026

I keep seeing it on code reviews. Proxy solid. Auth on every Server Component. Header direction correct. Then I look at the Server Actions. No au...
Collapse
 
webdeveloperhyper profile image
Web Developer Hyper

Nice Next.js 16 debugging as usual! 😃

I still sometimes open DevTools, wonder why there are no logs, and then realize that Next.js is running on the server instead of the browser. 😅 It's always a bit tricky for me.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Ha yeah that one gets everyone. console.log on the server just disappears into the void.

Collapse
 
hemapriya_kanagala profile image
Hemapriya Kanagala

Shubhra, I didn't know about this one.

The part about treating Server Actions like public-facing endpoints makes a lot of sense when you think about it, but I can also see why it's something people overlook.

Thanks for putting this together and walking through the examples.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Glad it helped. That endpoint framing is what clicked for me too. Once you think of it that way the auth check stops feeling optional.

The examples are just stuff I kept running into on real codebases so figured they were worth writing up.

Collapse
 
nazar-boyko profile image
Nazar Boyko

You can make that ownership check even harder to forget by folding it into the write itself. Instead of findUnique, compare authorId, then delete, do db.post.deleteMany({ where: { id: postId, authorId: session.userId } }) and treat a count of 0 as your "not found". A non-owner matches zero rows, the DB enforces ownership in one round trip, and there's no separate check sitting there waiting to get dropped during a refactor. Same idea works for update. verifySession still belongs up top, of course, this just collapses the check into the mutation itself. The point about Server Actions being public endpoints is one more people need to hear, the cURL line should be on a poster somewhere.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Good trick, but I kept the checks separate deliberately. When deleteMany returns 0 you lose the reason. Was the post missing or did they not own it? For logging and auditing that distinction actually matters.

An explicit ownership check is also harder to miss in a code review than an authorId tucked inside a where clause, which is kind of the whole point of the DAL pattern.

Collapse
 
voltagegpu profile image
VoltageGPU

As a backend-focused developer working with secure compute environments, I've seen similar patterns in API-layer auth checks—sometimes the headers aren't enough if you're not validating the request context deeply. For server actions, I always double-check that the session token isn't just present, but also scoped correctly to the user's permissions. It's easy to miss when things are abstracted, but critical in preventing privilege escalation.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Exactly right and that's the distinction the post tries to make clear. Checking that a token exists is just the entry check. verifySession() confirms who you are but that alone doesn't tell you anything about what that user can actually touch or whether they have permission to do it. The ownership check in the DAL is where the scope gets enforced, making sure the authenticated user actually owns the specific resource they're trying to mutate. Just having a valid token isn't enough. Both together, not just one.

Collapse
 
mudassirworks profile image
Mudassir Khan

the 'return a DTO not the raw record' section is the one that's bitten us most in production. we had a profile update action that returned the full user object, including a lastPasswordChangedAt field we'd added for compliance that hadn't been communicated to the frontend team. wasn't sensitive on its own but it showed up in network traces during a security audit and started a whole conversation we weren't ready to have.

explicit select in the DAL is now a PR requirement for us. the build doesn't catch it but code review does. how are you enforcing the DTO shape — TypeScript return types, a transform layer, or just convention and review?

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That audit story is exactly the kind of thing that doesn't feel like a problem until someone's asking questions in a meeting you weren't ready for.

Solo here so no PR gate, the explicit select in the DAL is doing all the scoping. The action only gets back what the query asked for so there's nothing extra to accidentally return. TypeScript return types on DAL functions would tighten it further but I haven't gone that route yet. For a team your PR requirement makes sense, the build won't catch it so someone has to.

Collapse
 
leob profile image
leob

Expert advice! But the fact that you need to do auth checks in your backend code is a no-brainer for every backend developer - the fact that people completely "forget" that in this scenario can't be a coincidence - it's because, with "use server", frontend devs have now 'suddenly' (and often unknowingly) become backend devs ... ;-)

Collapse
 
shubhradev profile image
Shubhra Pokhariya

Exactly, and use server is the reason it keeps happening. You write what looks like a regular async function and Next.js quietly turns it into a public HTTP endpoint. Nobody sees that directive and thinks backend security. The abstraction hides what's actually going on underneath.

Collapse
 
yune120 profile image
Yunetzi

Counterintuitive: the bigger risk isn't missing client checks; it's trusting client state. LocalStorage can be forged. Ship real server-side auth and use UI hints only for UX, not gating access.

Collapse
 
shubhradev profile image
Shubhra Pokhariya

That's the exact boundary the series is built around. Client side auth, whether it's localStorage, a context provider, or a button you hide based on role, is UX. It keeps the interface clean. It is not a security control.

verifySession() in every Server Action, ownership checks in the DAL, server-only so the auth logic never touches the browser. The cookie is just transport. The verification happens server side every time regardless of what the client is showing.

Collapse
 
laxmansubadi profile image
laxman Subedi

Nice product. The dashboard looks clean and the concept of finding issues before production is valuable.