R
Rishtaara
Next.js: Zero to Full-Stack
Lesson 29 of 42Article20 min

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
Tip: Never commit AUTH_SECRET or OAuth client secrets. Generate AUTH_SECRET with openssl rand -base64 32.
Auth.js sign-in to protected dashboard

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.

auth.ts — Auth.js v5 config
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",
  },
});
app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth";

export const { GET, POST } = handlers;
.env.local (never commit)
AUTH_SECRET=your-long-random-secret
AUTH_GOOGLE_ID=your-google-client-id
AUTH_GOOGLE_SECRET=your-google-client-secret

Protect 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.

Tip: Prefer auth() in Server Components and Server Actions. Use useSession only when a Client Component must react live to session changes.
app/dashboard/page.tsx — server-side guard
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>
  );
}
Login button (Server Action)
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.

Server Action that requires login
"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.

Tip: Combine middleware for quick redirects with auth() inside pages and actions for real authorization. Middleware alone is not enough for every security check.
middleware.ts — protect /dashboard/*
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*"],
};