Adding Authentication with Auth.js (NextAuth)
Auth.js (formerly NextAuth.js) handles sign-in, sessions, and provider logins (Google, GitHub, credentials) so you do not build password security from scratch.
Auth.js / NextAuth — what it does
Auth.js (formerly NextAuth.js) handles sign-in, sessions, and provider logins (Google, GitHub, credentials) so you do not build password security from scratch.
In the App Router you add a route handler at app/api/auth/[...nextauth]/route.ts, a shared auth config, and optional proxy/middleware to protect pages.
Real-life example: Auth.js is the reception desk at an office. It checks your ID (Google/GitHub), gives you a visitor badge (session), and decides which floors you may enter (protected routes).
- Install: npm install next-auth@beta (Auth.js v5 for App Router)
- Set AUTH_SECRET and provider keys in .env.local
- Export handlers, auth, signIn, signOut from one auth.ts file
Minimal Auth.js setup
Create auth.ts at the project root (or src/). Wire Google (or GitHub) as a provider. Re-export the route handlers for the catch-all API route.
Real-life example: Setting up Auth.js is like printing visitor badges once, then using the same badge printer at every entrance.
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID!,
clientSecret: process.env.AUTH_GOOGLE_SECRET!,
}),
],
pages: {
signIn: "/login",
},
});import { handlers } from "@/auth";
export const { GET, POST } = handlers;AUTH_SECRET=your-long-random-secret
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secretProtect dashboard routes
In a Server Component, call auth(). If there is no session, redirect to /login. This keeps protection on the server — clients cannot skip it by editing React state.
Real-life example: Checking auth() on the dashboard page is like a guard at the VIP lounge door — no badge, no entry, even if you walk confidently.
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user) {
redirect("/login");
}
return (
<main>
<h1>Welcome, {session.user.name}</h1>
<p>Email: {session.user.email}</p>
</main>
);
}import { signIn } from "@/auth";
export function SignInGoogle() {
return (
<form
action={async () => {
"use server";
await signIn("google", { redirectTo: "/dashboard" });
}}
>
<button type="submit">Continue with Google</button>
</form>
);
}Server Actions + session
Inside a Server Action, call auth() again before mutating data. Never trust a userId sent from the client — always read it from the session.
Real-life example: Checking the session inside a Server Action is confirming the signature on a cheque before cashing it — not just reading the name printed on the form.
"use server";
import { auth } from "@/auth";
import { revalidatePath } from "next/cache";
export async function saveCourseNote(note: string) {
const session = await auth();
if (!session?.user?.email) {
throw new Error("You must be signed in.");
}
// save note for session.user.email only
await db.notes.create({
data: { email: session.user.email, body: note },
});
revalidatePath("/dashboard");
}Proxy / middleware protection
For many routes at once, use middleware.ts (or proxy.ts in newer Next.js) to check the session cookie and redirect before the page runs. Match only the paths you care about.
Real-life example: Middleware is the building security gate. One check covers every office on floors 3–5, instead of putting a guard in each room.
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const isLoggedIn = !!req.auth;
const isDashboard = req.nextUrl.pathname.startsWith("/dashboard");
if (isDashboard && !isLoggedIn) {
const login = new URL("/login", req.nextUrl.origin);
login.searchParams.set("callbackUrl", req.nextUrl.pathname);
return NextResponse.redirect(login);
}
return NextResponse.next();
});
export const config = {
matcher: ["/dashboard/:path*"],
};