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:
- A single
auth()call that works everywhere — Server Components, Route Handlers, Server Actions, and Middleware — without passing the config object around. - 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.
- Framework agnostic core. Auth.js v5 works with Next.js, SvelteKit, and other frameworks from the same package (
next-authis 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
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:
The four exports are the entire Auth.js API surface:
handlers— theGETandPOSTRoute Handlers for OAuth callbacks and API endpointsauth— 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:
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:
Server Action:
Route Handler:
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.
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:
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.
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 nearestunauthorized.tsx.forbidden()— the user is authenticated but lacks permission (403). Renders the nearestforbidden.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:
Sign-out:
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:
For more sophisticated RBAC (hierarchical roles, permission sets, resource-level access), consider abstracting the checks:
Environment Variables
Auth.js requires specific environment variables:
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.
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 & RegisterDiscussion
0Join the discussion