Module P-4·12 min read

JWT vs database sessions, OAuth and credentials providers, auth() in Server Components and Actions, middleware-based route protection, RBAC, and the new forbidden()/unauthorized() auth interrupt system.

Written by Jatin Jain Saraf · Architect of SupraScan

P-4 — 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 is built on a shared, framework-agnostic core (@auth/core), with a separate package per framework: next-auth for Next.js, @auth/sveltekit for SvelteKit, @auth/express for Express, and so on. Next.js's own integration is the next-auth package itself, not a re-export of some other Next.js-specific package.

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

npm install next-auth@beta

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:

// auth.ts (at the project root, alongside next.config.ts)
 
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
import Credentials from 'next-auth/providers/credentials';
import { db } from '@/lib/db';
import bcrypt from 'bcryptjs';
 
export const { handlers, auth, signIn, signOut } = NextAuth({
  providers: [
    GitHub({
      clientId: process.env.AUTH_GITHUB_ID,
      clientSecret: process.env.AUTH_GITHUB_SECRET,
    }),
    Credentials({
      credentials: {
        email: { label: 'Email', type: 'email' },
        password: { label: 'Password', type: 'password' },
      },
      async authorize(credentials) {
        if (!credentials?.email || !credentials?.password) return null;
 
        const user = await db.users.findUnique({
          where: { email: credentials.email as string },
        });
 
        if (!user || !user.passwordHash) return null;
 
        const valid = await bcrypt.compare(
          credentials.password as string,
          user.passwordHash
        );
 
        if (!valid) return null;
 
        return { id: user.id, email: user.email, name: user.name, role: user.role };
      },
    }),
  ],
  callbacks: {
    jwt({ token, user }) {
      // Persist the role in the JWT on first sign-in
      if (user) token.role = (user as any).role;
      return token;
    },
    session({ session, token }) {
      // Make role available in session.user
      if (token.role) session.user.role = token.role as string;
      return session;
    },
  },
  pages: {
    signIn: '/auth/login',
    error: '/auth/error',
  },
});

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:

// app/api/auth/[...nextauth]/route.ts
 
import { handlers } from '@/auth';
 
export const { GET, POST } = handlers;

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:

// app/dashboard/page.tsx
 
import { auth } from '@/auth';
import { redirect } from 'next/navigation';
 
export default async function DashboardPage() {
  const session = await auth();
  if (!session) redirect('/auth/login');
 
  return <h1>Welcome, {session.user.name}</h1>;
}

Server Action:

// app/actions/posts.ts
'use server';
 
import { auth } from '@/auth';
 
export async function createPost(formData: FormData) {
  const session = await auth();
  if (!session?.user) throw new Error('Unauthorized');
 
  // session.user.id, session.user.email, session.user.role are available
  await db.posts.create({
    data: {
      title: formData.get('title') as string,
      authorId: session.user.id,
    },
  });
}

Route Handler:

// app/api/profile/route.ts
 
import { auth } from '@/auth';
 
export async function GET() {
  const session = await auth();
  if (!session) return new Response('Unauthorized', { status: 401 });
  return Response.json(session.user);
}

Sign in to keep reading

The rest of this module is free — sign in with Google to unlock it and track your progress.

Sign in & Register

Discussion

0

Join the discussion

Loading comments...

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