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