Skip to main content

Next.js Authentication Patterns

Answer

Authentication in Next.js involves protecting routes, managing sessions, and integrating with auth providers. The App Router introduces new patterns with Server Components and middleware.

Auth Architecture

NextAuth.js (Auth.js) Setup

// app/api/auth/[...nextauth]/route.ts
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";

export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Credentials({
credentials: {
email: {},
password: {},
},
async authorize(credentials) {
const user = await validateCredentials(credentials);
return user;
},
}),
],
callbacks: {
async session({ session, token }) {
session.user.id = token.sub;
return session;
},
},
});

export const { GET, POST } = handlers;

Middleware Protection

// middleware.ts
import { auth } from "./auth";

export default auth((req) => {
const isLoggedIn = !!req.auth;
const isProtected = req.nextUrl.pathname.startsWith("/dashboard");
const isAuthPage = req.nextUrl.pathname.startsWith("/login");

if (isProtected && !isLoggedIn) {
return Response.redirect(new URL("/login", req.nextUrl));
}

if (isAuthPage && isLoggedIn) {
return Response.redirect(new URL("/dashboard", req.nextUrl));
}
});

export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
};

Server Component Auth

// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export default async function Dashboard() {
const session = await auth();

if (!session?.user) {
redirect("/login");
}

return (
<div>
<h1>Welcome, {session.user.name}</h1>
<p>Email: {session.user.email}</p>
</div>
);
}

Client Component Auth

"use client";
import { useSession } from "next-auth/react";
import { signIn, signOut } from "next-auth/react";

export function AuthButton() {
const { data: session, status } = useSession();

if (status === "loading") {
return <span>Loading...</span>;
}

if (session) {
return (
<>
<span>{session.user?.name}</span>
<button onClick={() => signOut()}>Sign Out</button>
</>
);
}

return <button onClick={() => signIn()}>Sign In</button>;
}

Session Provider

// app/layout.tsx
import { SessionProvider } from "next-auth/react";
import { auth } from "@/auth";

export default async function RootLayout({ children }) {
const session = await auth();

return (
<html>
<body>
<SessionProvider session={session}>{children}</SessionProvider>
</body>
</html>
);
}

JWT vs Database Sessions

// JWT (stateless, stored in cookie)
export const { handlers, auth } = NextAuth({
session: { strategy: "jwt" },
// No database needed for session
callbacks: {
async jwt({ token, user }) {
if (user) token.role = user.role;
return token;
},
async session({ session, token }) {
session.user.role = token.role;
return session;
},
},
});

// Database Sessions (stateful)
export const { handlers, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "database" },
// Session stored in database
});

Role-Based Access

// Middleware
export default auth((req) => {
const role = req.auth?.user?.role;
const isAdmin = req.nextUrl.pathname.startsWith("/admin");

if (isAdmin && role !== "admin") {
return Response.redirect(new URL("/unauthorized", req.nextUrl));
}
});

// Server Component
async function AdminPage() {
const session = await auth();

if (session?.user?.role !== "admin") {
return <div>Access Denied</div>;
}

return <AdminDashboard />;
}

Server Actions with Auth

"use server";
import { auth } from "@/auth";
import { redirect } from "next/navigation";

export async function createPost(formData: FormData) {
const session = await auth();

if (!session?.user) {
redirect("/login");
}

await db.posts.create({
data: {
title: formData.get("title"),
authorId: session.user.id,
},
});

revalidatePath("/posts");
}

CSRF Protection

// Server Actions have built-in CSRF protection
// For API routes, use tokens

import { cookies } from "next/headers";
import { createHash, randomBytes } from "crypto";

function generateCSRFToken() {
const token = randomBytes(32).toString("hex");
const hash = createHash("sha256").update(token).digest("hex");
cookies().set("csrf_token", hash, { httpOnly: true });
return token;
}

function validateCSRFToken(token: string) {
const hash = createHash("sha256").update(token).digest("hex");
const storedHash = cookies().get("csrf_token")?.value;
return hash === storedHash;
}

Key Points

  • Use middleware for route protection
  • auth() for server-side auth checks
  • useSession() for client-side auth state
  • NextAuth.js handles OAuth and credentials
  • JWT for stateless, database for stateful sessions
  • Server Actions have built-in CSRF protection
  • Combine middleware + component checks for security