Module P-6·23 min read

The middleware execution model, matcher config, cookies()/headers() write constraints, userAgent() for device routing, draftMode() for CMS preview, and the 1ms performance budget.

P-3 — Authentication with Auth.js (NextAuth v5)

Who this is for: Practitioners building applications that require user identity — login, session management, route protection, and role-based access. Auth.js v5 (formerly NextAuth) was rebuilt from scratch for the App Router and Next.js 15. If you've used NextAuth v4 before, almost everything changed. If you're starting fresh, this is the current standard.


What Changed in v5

NextAuth v4 was designed for the Pages Router. It worked in the App Router but with friction — getServerSession() had to be called with the same config object everywhere, the session wasn't naturally available in Server Components, and middleware integration was awkward.

Auth.js v5 was rewritten for the App Router with three design goals:

  1. A single auth() call that works everywhere — Server Components, Route Handlers, Server Actions, and Middleware — without passing the config object around.
  2. First-class edge runtime support. The middleware-based session check runs at the CDN edge with near-zero latency, not in a full Node.js runtime.
  3. Framework agnostic core. Auth.js v5 works with Next.js, SvelteKit, and other frameworks from the same package (next-auth is a re-export of @auth/nextjs).

The migration from v4 to v5 is significant — different import paths, different config structure, different session API. This module covers v5 exclusively.


Installation and Setup

bash

Auth.js v5 uses the @beta tag as of late 2025 — it's stable and used in production, but the version tag reflects active development. Check npm show next-auth dist-tags for the current recommendation.

Create the core auth config:

ts

The four exports are the entire Auth.js API surface:

  • handlers — the GET and POST Route Handlers for OAuth callbacks and API endpoints
  • auth — the session retrieval function (works everywhere)
  • signIn — programmatic sign-in (Server Actions)
  • signOut — programmatic sign-out (Server Actions)

The Route Handler

Auth.js needs a [...nextauth] catch-all route to handle OAuth redirects, callbacks, and the sign-in/sign-out API:

ts

That's the entire file. handlers contains the fully configured GET and POST functions.


Reading the Session

The auth() function retrieves the current session. It works identically in every context:

Server Component:

tsx

Server Action:

ts

Route Handler:

ts

One function, same interface everywhere. No passing config objects, no getServerSession(authOptions) calls.


Session Strategies: JWT vs Database

Auth.js supports two session storage strategies:

JWT (default): Session data is stored in an encrypted cookie. No database reads on every request — the session is decoded from the cookie. Fast, works at the edge, stateless.

Database sessions: Session data is stored in a database. The cookie contains only a session ID. Every auth() call queries the database. Supports instant session revocation (delete the row, user is logged out immediately). Requires an Auth.js database adapter.

ts

When to use JWT: Personal projects, most SaaS applications, applications where session invalidation latency is acceptable (the JWT expires naturally).

When to use database sessions: Applications with strict security requirements (banking, healthcare), when you need instant logout across all devices, when you need to store large amounts of session data that would exceed cookie size limits.

For most applications, JWT is the right choice.


Extending the Session Type

TypeScript will complain that session.user.role doesn't exist on the default Session type. Fix this by extending the Auth.js types:

ts

This augments the Auth.js types globally so session.user.id and session.user.role are typed correctly throughout the application.


Middleware Route Protection — The Edge Layer

Middleware runs at the CDN edge before every request — before the page renders, before Server Components execute, before any Route Handlers run. This makes it the right place for session-based redirects: the check is cheap (JWT decryption, no database), happens as early as possible, and never renders a page that the user shouldn't see.

ts

Auth.js v5 wraps your middleware function with session decoding — request.auth contains the session (or null) without you writing any JWT decryption code.

The matcher controls which routes trigger the middleware. The pattern above skips static files, images, and the auth API routes themselves (which must be accessible without a session). Always be specific with matchers — running auth middleware on every static file request wastes compute.


forbidden() and unauthorized() — Auth Interrupts (Next.js 15)

Next.js 15 introduced two new control-flow functions specifically for auth scenarios:

  • unauthorized() — the user is not authenticated (401). Renders the nearest unauthorized.tsx.
  • forbidden() — the user is authenticated but lacks permission (403). Renders the nearest forbidden.tsx.
tsx
tsx
tsx

Before unauthorized() and forbidden(), the pattern was redirect('/login') for both cases. The new functions are semantically correct (401 vs 403 status codes), and they render dedicated pages rather than redirecting — which means the user sees the right message rather than landing on a generic login page when they're already logged in but lack permission.


Sign-In and Sign-Out

Sign-in page:

tsx

Sign-out:

tsx

Both signIn and signOut are Server Actions — they handle the redirect internally. No router.push(), no manual cookie clearing.


RBAC — Role-Based Access Control

With role in the JWT (set up in the callbacks above), RBAC flows naturally:

tsx

For more sophisticated RBAC (hierarchical roles, permission sets, resource-level access), consider abstracting the checks:

ts
ts

Environment Variables

Auth.js requires specific environment variables:

bash

AUTH_SECRET is the only hard requirement. Generate it with openssl rand -base64 32 and never commit it. On Vercel and most deployment platforms, set it in the environment variables dashboard, not in .env files.


Where We Go From Here

P-4 covers database integration with Prisma — setting up the schema for the user/session tables Auth.js needs (if using database sessions), the critical connection pooling issue that kills serverless applications at scale, and the full mutation pattern that connects Prisma to Server Actions.

The auth patterns from this module are the prerequisite for everything in the Architect phase that involves user-specific data — personalised pages, user-scoped caching, tenant isolation in multi-zone architectures.


Knowledge Check

What is one of the key design goals and improvements in Auth.js v5 compared to NextAuth v4?


Which of the following is true about session storage strategies in Auth.js?


What are the two new control-flow functions introduced in Next.js 15 for handling auth scenarios semantically?

Test your knowledge with more question sets

Sign in to access a wider variety of questions and get notified when new practice sets are added to this module.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

© 2026 Jatin Jain Saraf (JJS). All rights reserved.