HomeBytesheetsRoadmapsUI TemplatesUI ToolsStack Discussion
5 Topics
Chapter•25 Minutes

7. Protect Pages and API Routes

Use server-side session checks and enforce authorization for every protected resource.

Protect a Server-Rendered Page

Use getServerSession(req, res, authOptions) in getServerSideProps. Redirect to sign-in if no session exists. Do not rely only on a client-side redirect because it is not a security boundary.

File: `pages/dashboard.js`
import { getServerSession } from "next-auth/next";
import { authOptions } from "./api/auth/[...nextauth]";

export default function Dashboard({ email }) {
  return <main><h1>Dashboard</h1><p>Signed in as {email}</p></main>;
}

export async function getServerSideProps(context) {
  const session = await getServerSession(context.req, context.res, authOptions);
  if (!session) return { redirect: { destination: "/auth/signin", permanent: false } };
  return { props: { email: session.user?.email ?? "" } };
}
💡 Do not put secrets or sensitive records in returned props because they are serialized to the browser.

Protect an API Route

Every protected API route must check session and authorization before returning data or performing mutations. Return 401 for unauthenticated requests and 403 for authenticated users without permission.

File: `pages/api/private-data.js`
import { getServerSession } from "next-auth/next";
import { authOptions } from "./auth/[...nextauth]";

export default async function handler(req, res) {
  const session = await getServerSession(req, res, authOptions);
  if (!session) return res.status(401).json({ error: "Unauthorized" });
  if (req.method !== "GET") {
    res.setHeader("Allow", "GET");
    return res.status(405).json({ error: "Method not allowed" });
  }
  return res.status(200).json({ message: "Protected data", userId: session.user.id });
}
💡 Adjust the relative import path for the actual route location, and validate inputs and permissions for every operation.

Role-Based Authorization

A role can be copied from a trusted backend into token/session claims. The browser-visible role may hide or show controls, but the server must check the role before privileged operations. For high-risk actions, fetch current permissions from the database or authorization service.

Admin-only API check
if (!session) return res.status(401).json({ error: "Unauthorized" });
if (session.user?.role !== "admin") {
  return res.status(403).json({ error: "Forbidden" });
}
// Perform the privileged operation only after the checks.
💡 Place this check before the privileged operation. Do not trust a role supplied by the browser.

Client-Side Guard for UX

Use useSession and a router redirect to improve navigation, but protect the API and data source independently. A client guard is not sufficient to secure a page or backend resource.

Client guard
import { useEffect } from "react";
import { useRouter } from "next/router";
import { useSession } from "next-auth/react";

export default function PrivatePanel() {
  const router = useRouter();
  const { data: session, status } = useSession();
  useEffect(() => {
    if (status === "unauthenticated") router.replace("/auth/signin");
  }, [status, router]);
  if (status !== "authenticated") return <p>Loading…</p>;
  return <p>Welcome {session.user?.name ?? session.user?.email}</p>;
}
💡 Use this for UI flow only; secure the server endpoints separately.

Cookies, CSRF, and HTTPS

NextAuth manages its authentication cookies and CSRF protections for built-in flows. Do not copy session cookies into localStorage or JavaScript. In production use HTTPS and secure cookie defaults. Cookie behavior depends on host, protocol, domain, SameSite, and Secure settings; customize only when you understand the implications.

Keep session cookies private

Never expose session cookies to client JavaScript or send them to unrelated services.