Authentication

Know who is asking on both sides of the boundary, and guard a page where it cannot be bypassed.

13of 16
≈ 12Min read
7Parts

Cookies can only be set in a server action or a route handler — not while rendering a page, because the HTML may already be streaming. That restriction is a feature: it forces the login to be a real request.

set it on the server, and keep it out of JavaScript
import { cookies } from "next/headers";

(await cookies()).set("sid", sessionId, {
  httpOnly: true,     // no script can read it — blunts XSS
  secure: true,       // HTTPS only
  sameSite: "lax",    // not sent on cross-site POSTs — blunts CSRF
  path: "/",
  maxAge: 60 * 60 * 24 * 7,
});

Reading it, everywhere

one helper, used on every side
// lib/auth.js
import { cache } from "react";
import { cookies } from "next/headers";

export const auth = cache(async () => {
  const sid = (await cookies()).get("sid")?.value;
  if (!sid) return null;

  return await getSession(sid);        // deduplicated across the whole render
});

// a server component
const session = await auth();

// a client component — it cannot read the cookie, so pass what it needs
<Avatar user={session ? { name: session.name } : null} />
Reading cookies makes the route dynamic

Any page that checks a session is rendered per request. That is correct — it just means the marketing pages should not call auth() for a header avatar. Put the avatar in a small client component fed by a cached endpoint, or accept the dynamic render deliberately.

Guard where the data is

  • A layout does not re-run on every navigation, and a route handler or an action is reached without it at all.
  • Check in the page, the action and the handler — wherever the data is touched.
  • Middleware is a gate that saves a render, not a permission system.
the layout is not a security boundary
// app/(app)/layout.tsx — convenient, and NOT enough on its own
const session = await auth();
if (!session) redirect("/login");

// app/(app)/doors/[id]/page.tsx — the check that actually protects the data
const session = await auth();
if (!session) redirect("/login");

const door = await db.doors.byId(id);
if (door.ownerId !== session.userId) notFound();     // not 403: do not confirm it exists

Three layers, on purpose

Only the last one is a security boundary. The first two exist so the experience is good.

LayerQuestionCost of getting it wrong
MiddlewareIs there a cookie at all?A wasted render
Page / layoutWho is this, and may they see this screen?A leaked screen
Action / handler / queryMay they do this, to this row?A leaked or destroyed record

Signing in and out

both are actions
"use server";

export async function signIn(prevState, formData) {
  const user = await verify(formData.get("email"), formData.get("password"));
  if (!user) return { error: "Invalid email or password" };      // the same message either way

  const sid = await createSession(user.id);
  (await cookies()).set("sid", sid, { httpOnly: true, secure: true, sameSite: "lax", path: "/" });

  redirect(formData.get("next") || "/dashboard");
}

export async function signOut() {
  const sid = (await cookies()).get("sid")?.value;
  if (sid) await destroySession(sid);      // delete it on the SERVER, not just the cookie
  (await cookies()).delete("sid");
  redirect("/login");
}

Libraries

Whatever you use, the rule above does not change: the check that matters happens next to the data.

  • Auth.js (NextAuth) — OAuth providers, sessions and callbacks. The obvious default for "sign in with Google".
  • Clerk / WorkOS / Supabase Auth — hosted, with the flows (reset, verification, MFA, organisations) already built.
  • Lucia, or your own — a session table and a cookie. Perfectly reasonable, and you own every edge case.

Never trust the client half

hiding a button is not a permission
// this is a nicety
{session.role === "admin" && <DeleteButton id={door.id} />}

// this is the control
"use server";
export async function deleteDoor(formData) {
  const session = await auth();
  if (session?.role !== "admin") throw new Error("Not allowed");
  …
}

Practice