Authentication in Next.js
How Auth Works Here
The flow is classic web auth: user signs in → server sets an HTTP-only session cookie → every request carries it → server code reads the session and decides. Because Next renders on the server, pages themselves can check auth before any HTML is sent — no flash of protected content.
Auth.js (NextAuth) Setup
npm install next-auth@beta// auth.js (project root)
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub, Google],
callbacks: {
session({ session, token }) {
session.user.id = token.sub; // expose what your app needs
return session;
},
},
});// app/api/auth/[...nextauth]/route.js
import { handlers } from "@/auth";
export const { GET, POST } = handlers;# .env.local
AUTH_SECRET=generate-with: npx auth secret
AUTH_GITHUB_ID=...
AUTH_GITHUB_SECRET=...OAuth providers (GitHub/Google) mean no passwords to store — the right default for most apps. Credentials and magic-link email providers exist when you need them.
Sign In / Out — Server Actions
import { signIn, signOut, auth } from "@/auth";
export default async function AuthButton() {
const session = await auth();
if (!session) {
return (
<form action={async () => { "use server"; await signIn("github"); }}>
<button>Sign in with GitHub</button>
</form>
);
}
return (
<form action={async () => { "use server"; await signOut(); }}>
<span>{session.user.name}</span>
<button>Sign out</button>
</form>
);
}Protecting Things — Check at Every Layer
// a page
export default async function DashboardPage() {
const session = await auth();
if (!session) redirect("/login");
...
}
// a server action
"use server";
export async function deletePost(id) {
const session = await auth();
if (!session) throw new Error("unauthorised");
...
}
// a route handler
export async function GET() {
const session = await auth();
if (!session) return NextResponse.json({ error: "unauthorised" }, { status: 401 });
...
}The rule: authorise where the data is accessed, not just where the link is hidden. Every action and handler re-checks.
Middleware — Edge-Level Gates
For blanket rules (“all of /dashboard requires login”), middleware runs before the route:
// middleware.js (project root)
export { auth as middleware } from "@/auth";
export const config = {
matcher: ["/dashboard/:path*", "/settings/:path*"],
};Treat middleware as UX (fast redirects), not as your only defence — keep the in-route checks.
Role-Based Access
const session = await auth();
if (session?.user?.role !== "admin") notFound(); // hide existence entirelyStore roles in your user table, surface them via the session callback, and check them exactly like the auth checks above.