Think of the last app you built with authentication. How many hoops did you jump through — passing props, syncing state, juggling client and server logic? With the Next.js App Router, auth finally clicks into place. Server components, middleware, and cookies-based logic let you handle authentication where it belongs: on the server, before the page ever renders.

This chapter covers the 2026 approach — using Auth.js (NextAuth v5), which introduced a completely new API. If your code still references getServerSession or authOptions, this guide will bring you up to date.

3D Next.js logo: white letter N on a black circular disc

Why Auth Works Differently in the App Router

The app/ directory removes the patterns that auth used to rely on:

  • No getServerSideProps — React Server Components replace it
  • No need to prop-drill session state — server components fetch it directly
  • Middleware (or Proxy in Next.js 16) runs at the edge, before any page renders, making it the right place for global route protection

The result is cleaner, server-driven auth with less boilerplate.

Auth Options in Next.js 15 and 16

MethodBest ForTools
Auth.js (NextAuth v5)OAuth, email, credentialsauth(), handlers
Custom cookie sessionsToken-based auth, RBACcookies() from next/headers
Middleware / Proxy onlyGlobal redirects, no librarymiddleware.ts or proxy.ts + cookies()

Auth.js (NextAuth v5) — The New API

NextAuth v5 ships as Auth.js and rewrites the configuration model. As of mid-2026, v5 is still published on the beta tag — next-auth@latest still resolves to v4. Install v5 explicitly:

npm install next-auth@beta

Step 1: Create auth.ts

Create a single auth.ts file at your project root. This replaces the old authOptions export:

// auth.ts
import NextAuth from "next-auth"
import GitHub from "next-auth/providers/github"

export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [GitHub],
  callbacks: {
    // Required if you re-export auth as middleware/proxy for auto-redirects
    authorized: async ({ auth }) => {
      return !!auth
    },
  },
})

Step 2: Add the Route Handler

// app/api/auth/[...nextauth]/route.ts
import { handlers } from "@/auth"

export const { GET, POST } = handlers

That’s it. No authOptions, no wrapping — re-export the handlers from your auth.ts file.

Step 3: Protect Server Components with auth()

The old getServerSession(authOptions) is gone. In v5, you call auth() directly:

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

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

  if (!session) redirect("/login")

  return <h1>Welcome, {session.user?.name}</h1>
}

One import, one call, no extra arguments.

Middleware (and Proxy) for Global Route Protection

For routes you want to protect without touching each page individually, wire Auth.js into the request pipeline.

Next.js 15 and earlier — use middleware.ts:

// middleware.ts
export { auth as middleware } from "@/auth"

export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*"],
}

Next.js 16+ — the file convention was renamed to proxy.ts:

// proxy.ts
export { auth as proxy } from "@/auth"

export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*"],
}

The matcher config tells Next.js which routes to run this on. With the authorized callback in auth.ts, unauthenticated users hitting any matched path are redirected to your sign-in page.

For custom redirect logic — for example, redirecting to different pages based on user role — wrap it instead:

// middleware.ts (Next.js 15) or proxy.ts default export (Next.js 16)
import { auth } from "@/auth"
import { NextResponse } from "next/server"

export default auth((req) => {
  const isLoggedIn = !!req.auth
  const isAdminRoute = req.nextUrl.pathname.startsWith("/admin")

  if (isAdminRoute && req.auth?.user?.role !== "admin") {
    return NextResponse.redirect(new URL("/unauthorized", req.url))
  }

  if (!isLoggedIn) {
    return NextResponse.redirect(new URL("/login", req.url))
  }
})

export const config = {
  matcher: ["/dashboard/:path*", "/admin/:path*"],
}

Cookies-Based Auth Without a Library

If you don’t need OAuth and want full control, you can handle auth purely with cookies:

// app/dashboard/page.tsx
import { cookies } from "next/headers"
import { redirect } from "next/navigation"

export default async function DashboardPage() {
  const cookieStore = await cookies() // cookies() is async in Next.js 15+
  const token = cookieStore.get("token")?.value

  if (!token) redirect("/login")

  return <h1>Secret dashboard</h1>
}

Note that cookies() is async in Next.js 15 and later — always await it.

Protecting Client Components

When you need session data in a client component, wrap your app with SessionProvider and use the useSession hook:

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

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html>
      <body>
        <SessionProvider>{children}</SessionProvider>
      </body>
    </html>
  )
}
// components/UserMenu.tsx
"use client"
import { useSession } from "next-auth/react"

export default function UserMenu() {
  const { data: session } = useSession()

  if (!session) return <a href="/login">Sign in</a>
  return <span>Welcome, {session.user?.name}</span>
}

Protecting Routes at the Layout Level

One underused pattern in the App Router: protecting an entire section by adding an auth check to its layout. Every page nested under that layout inherits the protection without repeating the check:

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

export default async function DashboardLayout({ children }: { children: React.ReactNode }) {
  const session = await auth()

  if (!session) redirect("/login")

  return <>{children}</>
}

Any page under app/dashboard/ is automatically protected.

What’s Next

Authentication in the App Router isn’t just an upgrade — it’s a simplification. Server-driven sessions, one auth() call, and middleware (or proxy) that runs before the page renders. No prop drilling, no sync issues.

If you’ve been following this series, here’s where we are:

  • Chapter 1 — pages/ vs app/, RSCs, and navigation
  • Chapter 2 — Dynamic routes, catch-alls, generateStaticParams
  • Chapter 3 — React Server Components
  • Chapter 4 — SSR vs SSG vs ISR
  • Chapter 5 — getServerSideProps + External APIs
  • Chapter 6 — Streaming + Suspense
  • Chapter 7 — You’re here

More chapters coming. Keep shipping.