Middleware is your front door. But you must also re-check auth inside every Server Action and any sensitive Server Component — middleware can be bypassed if a route is misconfigured.
1// lib/auth-guard.ts
2import "server-only";
3import { redirect } from "next/navigation";
4import { getCurrentUser } from "@/lib/auth";
5
6export async function requireUser() {
7 const user = await getCurrentUser();
8 if (!user) redirect("/login");
9 return user;
10}
11
12export async function requireAdmin() {
13 const user = await requireUser();
14 if (user.role !== "ADMIN") redirect("/403");
15 return user;
16}1// app/admin/page.tsx
2import { requireAdmin } from "@/lib/auth-guard";
3
4export default async function AdminPage() {
5 const admin = await requireAdmin();
6 return <h1>Hi, {admin.name} — you're an admin.</h1>;
7}1// app/courses/actions.ts
2"use server";
3import { requireUser } from "@/lib/auth-guard";
4
5export async function enroll(courseId: string) {
6 const user = await requireUser();
7 return db.enrollment.create({ data: { userId: user.id, courseId } });
8}Never trust a userId passed from the client. Always read it from the session.
1"use server";
2export async function updateCourse(id: string, data: CourseInput) {
3 const user = await requireUser();
4 const course = await db.course.findUnique({ where: { id }, select: { createdById: true } });
5 if (!course) notFound();
6 if (course.createdById !`== user.id && user.role !==` "ADMIN") {
7 throw new Error("Forbidden");
8 }
9 return db.course.update({ where: { id }, data });
10}The pattern:
requireUser).For complex apps, group rules in one place:
1// lib/permissions.ts
2import type { User, Course } from "@prisma/client";
3
4export const can = {
5 editCourse: (u: User | null, c: Course) =>
6 !!u && (u.role =`== "ADMIN" || u.id ==`= c.createdById),
7 deleteCourse: (u: User | null, c: Course) =>
8 !!u && u.role === "ADMIN",
9};1{can.editCourse(user, course) && <EditButton id={course.id} />}Same module is reused server- and client-side (rules are pure functions — fine to ship).
Server Actions are immune to traditional CSRF because:
Route handlers that perform mutations still need their own CSRF checks if you're not using SameSite cookies.
After password changes, role changes, or account deletes, rotate the session:
1"use server";
2export async function changePassword(input: ChangePassword) {
3 const user = await requireUser();
4 await db.user.update({ where: { id: user.id }, data: { passwordHash: await hash(input.newPassword) } });
5 await createSession({ uid: user.id, role: user.role }); // new JWT, invalidates the old one
6}