Topic 10 / 13

Authentication in Next.js

~11 min read  //  Next.js Series  //  Coding India

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 entirely

Store roles in your user table, surface them via the session callback, and check them exactly like the auth checks above.