Claude Skill

api-auth-nextauth

Auth.js (NextAuth v5) authentication patterns - configuration, providers, session strategies, middleware, database adapters, role-based access, Edge compatibility

LLM Mart · 0 points · 0 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download agents-inc-skills-dist_plugins_api-auth-nextauth_skills_api-auth-nextauth-3a51ef5.zip · 20 KB
Part of agents-inc/skills — 130 skills

Install

skills CLI npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-auth-nextauth/skills/api-auth-nextauth
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
Git git clone https://github.com/agents-inc/skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Auth.js (NextAuth v5) Patterns

Quick Guide: Configure Auth.js in a root auth.ts file exporting { auth, handlers, signIn, signOut } from NextAuth(). Use the unified auth() function everywhere (Server Components, Route Handlers, middleware). Default session strategy is JWT (cookie-based); add a database adapter for persistent sessions. Protect routes via middleware or per-page auth() checks.


<critical_requirements>

CRITICAL: Before Using This Skill

All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering, import type, named constants)

(You MUST configure Auth.js in a root auth.ts file and export { auth, handlers, signIn, signOut } from NextAuth())

(You MUST use the unified auth() function for server-side session access - NOT the deprecated getServerSession(), getSession(), or getToken())

(You MUST use AUTH_SECRET environment variable - NEXTAUTH_SECRET is deprecated in v5)

(You MUST use AUTH_ prefixed environment variables for provider credentials (e.g., AUTH_GITHUB_ID, AUTH_GITHUB_SECRET) - they are auto-detected)

(You MUST split auth config into auth.config.ts (Edge-compatible) and auth.ts (with adapter) when using database sessions with middleware)

(You MUST check session inside Server Actions and API routes - middleware alone is NOT sufficient for authorization)

</critical_requirements>


Auto-detection: Auth.js, NextAuth, next-auth, authjs, auth.ts, auth.config.ts, NextAuth(), signIn, signOut, auth(), handlers, SessionProvider, useSession, AUTH_SECRET, OAuth provider, credentials provider, database adapter, @auth/prisma-adapter, @auth/drizzle-adapter, authorized callback, jwt callback, session callback, proxy auth, middleware auth

When to use:

  • Adding authentication to Next.js, SvelteKit, Express, or Qwik apps
  • Implementing OAuth login (GitHub, Google, Discord, etc.) with 80+ built-in providers
  • Building email/magic link authentication flows
  • Need JWT or database-backed session management
  • Projects requiring Edge-compatible middleware authentication

When NOT to use:

  • Building a custom auth system from scratch (Auth.js is opinionated)
  • Need fine-grained organization/team management out of the box
  • Mobile-only apps without web frontend
  • Need self-hosted auth with plugin architecture

Key patterns covered:

  • Auth configuration (auth.ts, auth.config.ts)
  • OAuth providers (GitHub, Google, Credentials, Email)
  • Session strategies (JWT vs database)
  • Session access (Server Components, Route Handlers, Client Components)
  • Middleware/proxy route protection
  • Database adapters (Prisma, Drizzle)
  • Callbacks (jwt, session, signIn, redirect)
  • Role-based access control
  • Edge compatibility split configuration

Detailed Resources:

  • For decision frameworks and anti-patterns, see reference.md

Core patterns:





<red_flags>

RED FLAGS

  • Using getServerSession(authOptions) -- deprecated in v5; use auth() from your auth.ts
  • Using NEXTAUTH_SECRET or NEXTAUTH_URL -- deprecated; use AUTH_SECRET (URL is auto-detected)
  • Credentials provider without rate limiting -- vulnerable to brute-force attacks
  • Exposing OAuth tokens to client via session callback -- keep accessToken/refreshToken server-side only
  • Middleware/proxy as sole authorization -- runs before rendering but does not replace per-route checks in Server Actions/API routes
  • Database adapter imported in middleware -- database ORMs can't run on Edge runtime (Next.js 14/15); split config into auth.config.ts + auth.ts
  • Wrapping signIn() in try/catch -- it throws a NEXT_REDIRECT exception internally (this is intentional)
  • JWT callback querying database on every call -- runs on EVERY auth() invocation; keep it lightweight

See reference.md for the complete anti-pattern list, gotchas, and migration table.

</red_flags>


<critical_reminders>

CRITICAL REMINDERS

All code must follow project conventions in CLAUDE.md

(You MUST configure Auth.js in a root auth.ts file and export { auth, handlers, signIn, signOut } from NextAuth())

(You MUST use the unified auth() function for server-side session access - NOT the deprecated getServerSession(), getSession(), or getToken())

(You MUST use AUTH_SECRET environment variable - NEXTAUTH_SECRET is deprecated in v5)

(You MUST use AUTH_ prefixed environment variables for provider credentials (e.g., AUTH_GITHUB_ID, AUTH_GITHUB_SECRET) - they are auto-detected)

(You MUST split auth config into auth.config.ts (Edge-compatible) and auth.ts (with adapter) when using database sessions with middleware)

(You MUST check session inside Server Actions and API routes - middleware alone is NOT sufficient for authorization)

Failure to follow these rules will cause authentication failures, expose deprecated patterns, or create security vulnerabilities.

</critical_reminders>

Files (skills)
  • examples
    • core.md 8.1 KB
      # Auth Configuration & Providers
      
      > Complete code examples for Auth.js configuration, providers, and callbacks. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Basic OAuth Configuration
      
      ### Good Example - Multi-Provider Setup
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import GitHub from "next-auth/providers/github";
      import Google from "next-auth/providers/google";
      import Discord from "next-auth/providers/discord";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        providers: [
          GitHub, // Auto-detects AUTH_GITHUB_ID, AUTH_GITHUB_SECRET
          Google, // Auto-detects AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET
          Discord, // Auto-detects AUTH_DISCORD_ID, AUTH_DISCORD_SECRET
        ],
        pages: {
          signIn: "/login", // Custom sign-in page
          error: "/auth/error", // Custom error page
        },
      });
      ```
      
      ```typescript
      // app/api/auth/[...nextauth]/route.ts
      import { handlers } from "@/auth";
      
      export const { GET, POST } = handlers;
      ```
      
      ```bash
      # .env.local
      AUTH_SECRET="npx auth secret"
      AUTH_GITHUB_ID="..."
      AUTH_GITHUB_SECRET="..."
      AUTH_GOOGLE_ID="..."
      AUTH_GOOGLE_SECRET="..."
      AUTH_DISCORD_ID="..."
      AUTH_DISCORD_SECRET="..."
      ```
      
      **Why good:** Providers auto-detect env vars (zero config), custom pages override defaults, single route file handles all auth endpoints
      
      ### Bad Example - v4 Style Configuration
      
      ```typescript
      // BAD: v4 pattern - authOptions export in API route
      // pages/api/auth/[...nextauth].ts
      import NextAuth from "next-auth";
      
      export const authOptions = {
        providers: [
          // ...
        ],
        secret: process.env.NEXTAUTH_SECRET, // BAD: deprecated env var
      };
      
      export default NextAuth(authOptions);
      ```
      
      **Why bad:** v4 pattern (authOptions export), deprecated `NEXTAUTH_SECRET`, Pages Router API route, no typed exports
      
      ---
      
      ## Pattern 2: Credentials Provider
      
      ### Good Example - Email/Password with Validation
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import Credentials from "next-auth/providers/credentials";
      import bcrypt from "bcryptjs";
      import { z } from "zod";
      
      const LoginSchema = z.object({
        email: z.string().email("Invalid email address"),
        password: z.string().min(8, "Password must be at least 8 characters"),
      });
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        providers: [
          Credentials({
            credentials: {
              email: { label: "Email", type: "email" },
              password: { label: "Password", type: "password" },
            },
            async authorize(credentials) {
              // Validate input
              const parsed = LoginSchema.safeParse(credentials);
              if (!parsed.success) return null;
      
              // Look up user
              const user = await db.user.findUnique({
                where: { email: parsed.data.email },
              });
              if (!user?.hashedPassword) return null;
      
              // Verify password
              const isValid = await bcrypt.compare(
                parsed.data.password,
                user.hashedPassword,
              );
              if (!isValid) return null;
      
              // Return user object (becomes `user` in jwt callback)
              return {
                id: user.id,
                name: user.name,
                email: user.email,
                role: user.role,
              };
            },
          }),
        ],
        session: { strategy: "jwt" }, // Credentials requires JWT strategy
      });
      ```
      
      **Why good:** Zod validates input before DB query, bcrypt for password verification, null return on any failure (doesn't reveal which step failed), explicit JWT strategy
      
      ### Bad Example - Insecure Credentials
      
      ```typescript
      // BAD: No validation, plain text comparison
      Credentials({
        async authorize(credentials) {
          const user = await db.user.findUnique({
            where: { email: credentials.email as string },
          });
          if (user?.password === credentials.password) {
            // BAD: plain text compare
            return user;
          }
          throw new Error("Invalid credentials"); // BAD: throws instead of returning null
        },
      });
      ```
      
      **Why bad:** No input validation, plain text password comparison, throwing error leaks information to client (return null instead)
      
      ---
      
      ## Pattern 3: Callbacks
      
      ### Good Example - JWT and Session Callbacks with Custom Data
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import GitHub from "next-auth/providers/github";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        providers: [GitHub],
        callbacks: {
          async jwt({ token, user, account }) {
            // `user` is only available on first sign-in
            if (user) {
              token.id = user.id;
              token.role = user.role ?? "user";
            }
            // `account` has OAuth tokens on first sign-in
            if (account) {
              token.accessToken = account.access_token;
            }
            return token;
          },
      
          async session({ session, token }) {
            // Expose only what the client needs
            session.user.id = token.id as string;
            session.user.role = token.role as string;
            // Do NOT expose accessToken to client
            return session;
          },
      
          async signIn({ user, account, profile }) {
            // Restrict to specific email domains
            if (account?.provider === "google") {
              return user.email?.endsWith("@company.com") ?? false;
            }
            return true;
          },
      
          async redirect({ url, baseUrl }) {
            // Prevent open redirect attacks
            if (url.startsWith("/")) return `${baseUrl}${url}`;
            if (new URL(url).origin === baseUrl) return url;
            return baseUrl;
          },
        },
      });
      ```
      
      **Why good:** Custom data flows from jwt -> session callback, sensitive tokens kept server-side, email domain restriction for corporate SSO, redirect validation prevents open redirects
      
      ### Bad Example - Leaking Tokens to Client
      
      ```typescript
      // BAD: Exposing OAuth access token to client
      callbacks: {
        async session({ session, token }) {
          session.accessToken = token.accessToken; // BAD: exposed to client
          session.refreshToken = token.refreshToken; // BAD: exposed to client
          return session;
        },
      }
      ```
      
      **Why bad:** Access and refresh tokens exposed to browser, allows token theft via XSS, should stay server-side only
      
      ---
      
      ## Pattern 4: Sign In / Sign Out
      
      ### Good Example - Server Actions for Auth
      
      ```typescript
      // components/auth-buttons.tsx
      import { signIn, signOut } from "@/auth";
      
      export function SignInButton({ provider = "github" }: { provider?: string }) {
        return (
          <form
            action={async () => {
              "use server";
              await signIn(provider, { redirectTo: "/dashboard" });
            }}
          >
            <button type="submit">Sign in with {provider}</button>
          </form>
        );
      }
      
      export function SignOutButton() {
        return (
          <form
            action={async () => {
              "use server";
              await signOut({ redirectTo: "/" });
            }}
          >
            <button type="submit">Sign out</button>
          </form>
        );
      }
      ```
      
      **Why good:** Server Actions for progressive enhancement (works without JS), `redirectTo` controls destination, imported from server-side `@/auth`
      
      ### Good Example - Client-Side Sign In
      
      ```typescript
      // components/client-auth.tsx
      "use client";
      
      import { signIn, signOut } from "next-auth/react";
      
      export function ClientSignIn() {
        return (
          <button onClick={() => signIn("github", { callbackUrl: "/dashboard" })}>
            Sign in
          </button>
        );
      }
      
      export function ClientSignOut() {
        return (
          <button onClick={() => signOut({ callbackUrl: "/" })}>
            Sign out
          </button>
        );
      }
      ```
      
      **Why good:** Client-side import from `next-auth/react` (not `@/auth`), `callbackUrl` for client-side redirect
      
      ---
      
      ## Pattern 5: TypeScript Type Extensions
      
      ### Good Example - Extending Session Types
      
      ```typescript
      // types/next-auth.d.ts
      import type { DefaultSession, DefaultJWT } from "next-auth";
      
      declare module "next-auth" {
        interface Session {
          user: {
            id: string;
            role: "admin" | "editor" | "user";
          } & DefaultSession["user"];
        }
      
        interface User {
          role?: "admin" | "editor" | "user";
        }
      }
      
      declare module "next-auth/jwt" {
        interface JWT extends DefaultJWT {
          id?: string;
          role?: "admin" | "editor" | "user";
          accessToken?: string;
        }
      }
      ```
      
      **Why good:** Type-safe custom fields on session and JWT, extends defaults rather than replacing, union type for role values
      
      ---
      
      _See [session.md](session.md) for session strategies and [middleware.md](middleware.md) for route protection._
      
    • database.md 4.8 KB
      # Database Adapters
      
      > Code examples for Auth.js database adapters - Prisma, Drizzle, session storage. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Prisma Adapter
      
      ### Good Example - Full Prisma Setup
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import GitHub from "next-auth/providers/github";
      import { PrismaAdapter } from "@auth/prisma-adapter";
      import { prisma } from "@/lib/prisma";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        adapter: PrismaAdapter(prisma),
        providers: [GitHub],
        session: { strategy: "database" },
        callbacks: {
          async session({ session, user }) {
            // With database sessions, `user` (not `token`) is available
            session.user.id = user.id;
            session.user.role = user.role;
            return session;
          },
        },
      });
      ```
      
      Auth.js requires four models in your schema: **User**, **Account**, **Session**, **VerificationToken**. See the [official Prisma adapter docs](https://authjs.dev/getting-started/adapters/prisma) for the complete schema. Key points:
      
      - `Account` has `@@unique([provider, providerAccountId])` composite key
      - `Session` has `sessionToken @unique` and `expires` field
      - `VerificationToken` has `@@unique([identifier, token])` composite key
      - All relations should use `onDelete: Cascade`
      - Add custom fields (e.g., `role`) to User model, then expose via session callback
      
      **Why good:** Session callback uses `user` (not `token`) for database strategy, cascade deletes clean up related records
      
      ---
      
      ## Pattern 2: Drizzle Adapter
      
      ### Good Example - Drizzle ORM with PostgreSQL
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import GitHub from "next-auth/providers/github";
      import { DrizzleAdapter } from "@auth/drizzle-adapter";
      import { db } from "@/lib/db";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        adapter: DrizzleAdapter(db),
        providers: [GitHub],
        session: { strategy: "database" },
      });
      ```
      
      The same four models (users, accounts, sessions, verificationTokens) are required. See the [official Drizzle adapter docs](https://authjs.dev/getting-started/adapters/drizzle) for the complete schema. Key points:
      
      - Import `AdapterAccountType` from `next-auth/adapters` for typed `account.type` field
      - Use `primaryKey({ columns: [account.provider, account.providerAccountId] })` composite key
      - Use `onDelete: "cascade"` on all foreign key references
      - Use `timestamp("emailVerified", { mode: "date" })` for proper Date handling
      
      **Why good:** Drizzle schema matches Auth.js model requirements, cascade deletes for data integrity, typed account type from Auth.js adapters
      
      ---
      
      ## Pattern 3: Email Provider with Database
      
      ### Good Example - Magic Link Authentication
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import Resend from "next-auth/providers/resend";
      import { PrismaAdapter } from "@auth/prisma-adapter";
      import { prisma } from "@/lib/prisma";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        adapter: PrismaAdapter(prisma), // Required for email provider
        providers: [
          Resend({
            // Auto-detects AUTH_RESEND_KEY
            from: "noreply@example.com",
          }),
        ],
      });
      ```
      
      **Why good:** Email provider requires database adapter (stores verification tokens), Resend auto-detects API key, minimal configuration needed
      
      ### Bad Example - Email Provider Without Adapter
      
      ```typescript
      // BAD: Email provider needs a database adapter
      export const { auth } = NextAuth({
        providers: [Resend({ from: "noreply@example.com" })],
        // Missing adapter - verification tokens can't be stored
      });
      ```
      
      **Why bad:** Email provider stores verification tokens in database; without adapter, magic links can't be verified
      
      ---
      
      ## Pattern 4: Custom User Fields
      
      ### Good Example - Extending User Model
      
      Add custom fields directly to your User model (e.g., `role`, `bio`, `onboardingComplete`), then expose them via the session callback:
      
      ```typescript
      // auth.ts - Expose custom fields in session
      export const { auth, handlers, signIn, signOut } = NextAuth({
        adapter: PrismaAdapter(prisma),
        providers: [GitHub],
        session: { strategy: "database" },
        callbacks: {
          async session({ session, user }) {
            // `user` is the full database User object
            session.user.id = user.id;
            session.user.role = user.role;
            session.user.onboardingComplete = user.onboardingComplete;
            return session;
          },
        },
      });
      ```
      
      ```typescript
      // types/next-auth.d.ts
      import type { DefaultSession } from "next-auth";
      
      declare module "next-auth" {
        interface Session {
          user: {
            id: string;
            role: string;
            onboardingComplete: boolean;
          } & DefaultSession["user"];
        }
      }
      ```
      
      **Why good:** Custom fields on User model, exposed via session callback, TypeScript types updated for type safety, database strategy gives access to full `user` object
      
      ---
      
      _See [patterns.md](patterns.md) for role-based access and account linking patterns._
      
    • middleware.md 7.9 KB
      # Route Protection & Middleware
      
      > Code examples for Auth.js route protection - middleware patterns, Edge compatibility, per-page checks. See [SKILL.md](../SKILL.md) for core concepts.
      
      > **Next.js 16 note:** `middleware.ts` is renamed to `proxy.ts` and the export is renamed to `proxy` in Next.js 16+. In Next.js 16, `proxy.ts` runs on **Node.js runtime** (not Edge), so the split config pattern is no longer required. Examples below use `middleware.ts` (Next.js 14/15). For Next.js 16+, rename to `proxy.ts` and export as `proxy`.
      
      ---
      
      ## Pattern 1: Basic Middleware Protection
      
      ### Good Example - Protect All Routes with Exceptions
      
      ```typescript
      // middleware.ts (Next.js 14/15) or proxy.ts (Next.js 16+)
      export { auth as middleware } from "@/auth";
      // Next.js 16+: export { auth as proxy } from "@/auth";
      
      export const config = {
        matcher: [
          // Match all routes except static files and public paths
          "/((?!api/auth|_next/static|_next/image|favicon.ico|public).*)",
        ],
      };
      ```
      
      ```typescript
      // auth.ts - with authorized callback
      import NextAuth from "next-auth";
      import GitHub from "next-auth/providers/github";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        providers: [GitHub],
        callbacks: {
          authorized({ auth, request }) {
            const isLoggedIn = !!auth?.user;
            const isPublicRoute = ["/", "/about", "/pricing"].includes(
              request.nextUrl.pathname,
            );
      
            if (isPublicRoute) return true;
            return isLoggedIn; // false redirects to sign-in
          },
        },
      });
      ```
      
      **Why good:** Centralized auth check for all routes, public routes explicitly allowed, `authorized` callback returns boolean (false = redirect to sign-in)
      
      ---
      
      ## Pattern 2: Custom Middleware with Redirect Logic
      
      ### Good Example - Role-Based Redirect
      
      ```typescript
      // middleware.ts (Next.js 14/15) or proxy.ts (Next.js 16+)
      import { auth } from "@/auth";
      import { NextResponse } from "next/server";
      
      const PUBLIC_ROUTES = ["/", "/about", "/pricing", "/login"];
      const ADMIN_ROUTES = ["/admin"];
      
      export default auth((req) => {
        const { nextUrl } = req;
        const isLoggedIn = !!req.auth;
        const isPublicRoute = PUBLIC_ROUTES.some((route) =>
          nextUrl.pathname.startsWith(route),
        );
        const isAdminRoute = ADMIN_ROUTES.some((route) =>
          nextUrl.pathname.startsWith(route),
        );
      
        // Public routes: always accessible
        if (isPublicRoute) return NextResponse.next();
      
        // Not logged in: redirect to login
        if (!isLoggedIn) {
          const loginUrl = new URL("/login", nextUrl.origin);
          loginUrl.searchParams.set("callbackUrl", nextUrl.pathname);
          return NextResponse.redirect(loginUrl);
        }
      
        // Admin routes: check role (available in JWT)
        if (isAdminRoute && req.auth?.user?.role !== "admin") {
          return NextResponse.redirect(new URL("/unauthorized", nextUrl.origin));
        }
      
        return NextResponse.next();
      });
      
      export const config = {
        matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
      };
      ```
      
      **Why good:** Named route arrays for maintainability, preserves callback URL for post-login redirect, role check in middleware for admin routes
      
      ### Bad Example - No Public Route Handling
      
      ```typescript
      // BAD: All routes require auth including landing page
      export { auth as middleware } from "@/auth";
      
      // No matcher config - catches everything
      // No authorized callback - defaults to requiring auth
      ```
      
      **Why bad:** Blocks public pages (landing, pricing, about), no matcher excludes static assets, forces login on every route
      
      ---
      
      ## Pattern 3: Edge-Compatible Split Configuration
      
      ### Good Example - Split Config for Database Sessions + Middleware
      
      ```typescript
      // auth.config.ts - Edge-compatible (NO database imports)
      import GitHub from "next-auth/providers/github";
      import type { NextAuthConfig } from "next-auth";
      
      const PUBLIC_ROUTES = ["/", "/about", "/login"];
      
      export const authConfig = {
        providers: [GitHub],
        pages: {
          signIn: "/login",
        },
        callbacks: {
          authorized({ auth, request }) {
            const isLoggedIn = !!auth?.user;
            const isPublic = PUBLIC_ROUTES.some((route) =>
              request.nextUrl.pathname.startsWith(route),
            );
            if (isPublic) return true;
            return isLoggedIn;
          },
        },
      } satisfies NextAuthConfig;
      ```
      
      ```typescript
      // auth.ts - Full config with database adapter (Node.js only)
      import NextAuth from "next-auth";
      import { PrismaAdapter } from "@auth/prisma-adapter";
      import { prisma } from "@/lib/prisma";
      import { authConfig } from "./auth.config";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        ...authConfig,
        adapter: PrismaAdapter(prisma),
        session: { strategy: "database" },
      });
      ```
      
      ```typescript
      // middleware.ts (Next.js 14/15) - Uses Edge-compatible config
      import NextAuth from "next-auth";
      import { authConfig } from "./auth.config";
      
      export const { auth: middleware } = NextAuth(authConfig);
      // Next.js 16+: export const { auth: proxy } = NextAuth(authConfig);
      // Note: In Next.js 16, proxy.ts runs on Node.js, so split config is optional
      
      export const config = {
        matcher: ["/((?!api/auth|_next/static|_next/image|favicon.ico).*)"],
      };
      ```
      
      **Why good:** `auth.config.ts` has no database imports (Edge-safe), `auth.ts` adds adapter for full Node.js runtime, middleware uses the slim Edge config. In Next.js 16+, `proxy.ts` runs on Node.js so you can use the full `auth.ts` directly.
      
      ### Bad Example - Database Adapter in Middleware (Next.js 14/15)
      
      ```typescript
      // BAD: Importing Prisma in middleware breaks Edge runtime (Next.js 14/15)
      // middleware.ts
      import { auth } from "@/auth"; // auth.ts imports PrismaAdapter
      
      export { auth as middleware };
      // ERROR: PrismaClient cannot run on Edge runtime
      // Note: This is NOT an issue in Next.js 16+ proxy.ts (runs on Node.js)
      ```
      
      **Why bad:** Prisma/Drizzle can't run on Edge runtime, middleware crashes at deploy time, must split config. In Next.js 16+, `proxy.ts` runs on Node.js so this limitation no longer applies.
      
      ---
      
      ## Pattern 4: Per-Page Protection
      
      ### Good Example - Authorization in Server Components
      
      ```typescript
      // app/admin/page.tsx
      import { auth } from "@/auth";
      import { redirect } from "next/navigation";
      
      export default async function AdminPage() {
        const session = await auth();
      
        if (!session?.user) {
          redirect("/login");
        }
      
        if (session.user.role !== "admin") {
          redirect("/unauthorized");
        }
      
        // Only admins reach here
        return (
          <div>
            <h1>Admin Dashboard</h1>
            <p>Welcome, {session.user.name}</p>
          </div>
        );
      }
      ```
      
      ### Good Example - Protected Server Action
      
      ```typescript
      // app/actions/admin-actions.ts
      "use server";
      
      import { auth } from "@/auth";
      
      export async function deleteUser(userId: string) {
        const session = await auth();
      
        if (!session?.user) {
          throw new Error("Authentication required");
        }
      
        if (session.user.role !== "admin") {
          throw new Error("Admin access required");
        }
      
        // Proceed with deletion
        await db.user.delete({ where: { id: userId } });
      
        return { success: true };
      }
      ```
      
      **Why good:** Defense in depth - middleware provides first check, Server Component/Action provides authorization check, both session and role verified
      
      ---
      
      ## Pattern 5: Protecting API Routes
      
      ### Good Example - Authenticated API Endpoint
      
      ```typescript
      // app/api/users/route.ts
      import { auth } from "@/auth";
      import { NextResponse } from "next/server";
      
      export const GET = auth(async function GET(req) {
        if (!req.auth?.user) {
          return NextResponse.json(
            { error: "Authentication required" },
            { status: 401 },
          );
        }
      
        if (req.auth.user.role !== "admin") {
          return NextResponse.json(
            { error: "Admin access required" },
            { status: 403 },
          );
        }
      
        const users = await db.user.findMany({
          select: { id: true, name: true, email: true, role: true },
        });
      
        return NextResponse.json(users);
      });
      ```
      
      **Why good:** Wrapped with `auth()` for automatic session injection, 401 for unauthenticated, 403 for unauthorized, proper HTTP status codes
      
      ---
      
      _See [database.md](database.md) for adapter setup and [patterns.md](patterns.md) for advanced auth patterns._
      
    • patterns.md 7.9 KB
      # Advanced Auth Patterns
      
      > Code examples for role-based access, magic links, account linking, and custom sign-in pages. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: Role-Based Access Control
      
      ### Good Example - RBAC with Auth.js
      
      ```typescript
      // lib/auth-utils.ts
      import { auth } from "@/auth";
      import { redirect } from "next/navigation";
      
      type Role = "admin" | "editor" | "user";
      
      export async function requireAuth() {
        const session = await auth();
        if (!session?.user) {
          redirect("/login");
        }
        return session;
      }
      
      export async function requireRole(requiredRole: Role) {
        const session = await requireAuth();
      
        const ROLE_HIERARCHY: Record<Role, number> = {
          admin: 3,
          editor: 2,
          user: 1,
        };
      
        const userLevel = ROLE_HIERARCHY[session.user.role as Role] ?? 0;
        const requiredLevel = ROLE_HIERARCHY[requiredRole];
      
        if (userLevel < requiredLevel) {
          redirect("/unauthorized");
        }
      
        return session;
      }
      ```
      
      ```typescript
      // app/admin/users/page.tsx
      import { requireRole } from "@/lib/auth-utils";
      
      export default async function AdminUsersPage() {
        const session = await requireRole("admin");
      
        const users = await db.user.findMany({
          select: { id: true, name: true, email: true, role: true },
        });
      
        return (
          <div>
            <h1>User Management</h1>
            <p>Logged in as: {session.user.name} ({session.user.role})</p>
            <table>
              <thead>
                <tr><th>Name</th><th>Email</th><th>Role</th></tr>
              </thead>
              <tbody>
                {users.map((user) => (
                  <tr key={user.id}>
                    <td>{user.name}</td>
                    <td>{user.email}</td>
                    <td>{user.role}</td>
                  </tr>
                ))}
              </tbody>
            </table>
          </div>
        );
      }
      ```
      
      **Why good:** Reusable auth helpers, role hierarchy allows admins to access editor pages too, named constant for hierarchy, redirect on insufficient permissions
      
      ---
      
      ## Pattern 2: Custom Sign-In Page
      
      ### Good Example - Branded Login Page
      
      ```typescript
      // auth.ts
      export const { auth, handlers, signIn, signOut } = NextAuth({
        providers: [
          GitHub,
          Google,
          Credentials({
            /* ... */
          }),
        ],
        pages: {
          signIn: "/login", // Custom login page
          error: "/login/error", // Custom error page
        },
      });
      ```
      
      ```typescript
      // app/login/page.tsx
      import { auth, signIn } from "@/auth";
      import { redirect } from "next/navigation";
      
      export default async function LoginPage({
        searchParams,
      }: {
        searchParams: Promise<{ callbackUrl?: string; error?: string }>;
      }) {
        const session = await auth();
        const { callbackUrl, error } = await searchParams;
      
        // Already logged in - redirect to callback or dashboard
        if (session?.user) {
          redirect(callbackUrl ?? "/dashboard");
        }
      
        return (
          <div className="login-container">
            <h1>Sign In</h1>
      
            {error && (
              <div className="error-banner" role="alert">
                {error === "OAuthAccountNotLinked"
                  ? "This email is already associated with another provider."
                  : "An error occurred during sign in."}
              </div>
            )}
      
            {/* OAuth providers */}
            <form
              action={async () => {
                "use server";
                await signIn("github", { redirectTo: callbackUrl ?? "/dashboard" });
              }}
            >
              <button type="submit">Continue with GitHub</button>
            </form>
      
            <form
              action={async () => {
                "use server";
                await signIn("google", { redirectTo: callbackUrl ?? "/dashboard" });
              }}
            >
              <button type="submit">Continue with Google</button>
            </form>
      
            {/* Credentials form */}
            <form
              action={async (formData: FormData) => {
                "use server";
                await signIn("credentials", {
                  email: formData.get("email"),
                  password: formData.get("password"),
                  redirectTo: callbackUrl ?? "/dashboard",
                });
              }}
            >
              <input type="email" name="email" placeholder="Email" required />
              <input type="password" name="password" placeholder="Password" required />
              <button type="submit">Sign in with Email</button>
            </form>
          </div>
        );
      }
      ```
      
      **Why good:** Redirects already-authenticated users, preserves callback URL, handles OAuth error codes, Server Actions for each provider, progressive enhancement
      
      ---
      
      ## Pattern 3: Account Linking
      
      ### Good Example - Controlled Account Linking
      
      ```typescript
      // auth.ts
      export const { auth, handlers, signIn, signOut } = NextAuth({
        providers: [GitHub, Google],
        callbacks: {
          async signIn({ user, account, profile }) {
            // Check if email already exists with a different provider
            if (account?.provider && user.email) {
              const existingUser = await db.user.findUnique({
                where: { email: user.email },
                include: { accounts: true },
              });
      
              if (existingUser) {
                const hasProvider = existingUser.accounts.some(
                  (a) => a.provider === account.provider,
                );
      
                if (!hasProvider) {
                  // Link the new provider to existing account
                  // Auth.js handles this automatically with allowDangerousEmailAccountLinking
                  // But you may want custom logic here
                }
              }
            }
            return true;
          },
        },
        // Enable automatic linking for same-email accounts
        // WARNING: Only enable if you trust ALL your OAuth providers to verify emails
        // allowDangerousEmailAccountLinking: true,
      });
      ```
      
      **Why good:** Explicit check for existing accounts, documented security implications, automatic linking commented out with warning
      
      ---
      
      ## Pattern 4: Session-Based Guards for Components
      
      ### Good Example - Conditional UI Based on Auth Status
      
      ```typescript
      // components/auth-guard.tsx
      import { auth } from "@/auth";
      
      interface AuthGuardProps {
        children: React.ReactNode;
        fallback?: React.ReactNode;
        requiredRole?: string;
      }
      
      export async function AuthGuard({
        children,
        fallback = null,
        requiredRole,
      }: AuthGuardProps) {
        const session = await auth();
      
        if (!session?.user) {
          return <>{fallback}</>;
        }
      
        if (requiredRole && session.user.role !== requiredRole) {
          return <>{fallback}</>;
        }
      
        return <>{children}</>;
      }
      ```
      
      ```typescript
      // app/page.tsx
      import { AuthGuard } from "@/components/auth-guard";
      
      export default function HomePage() {
        return (
          <div>
            <h1>Welcome</h1>
      
            <AuthGuard fallback={<p>Sign in to see your dashboard</p>}>
              <DashboardWidget />
            </AuthGuard>
      
            <AuthGuard requiredRole="admin">
              <AdminPanel />
            </AuthGuard>
          </div>
        );
      }
      ```
      
      **Why good:** Reusable Server Component guard, optional role check, fallback content for unauthenticated users, composable
      
      ---
      
      ## Pattern 5: Rate Limiting Sign-In
      
      ### Good Example - Rate Limiting Credentials Provider
      
      ```typescript
      // lib/rate-limit.ts
      const WINDOW_MS = 15 * 60 * 1000; // 15 minutes
      const MAX_ATTEMPTS = 5;
      
      const attempts = new Map<string, { count: number; resetTime: number }>();
      
      export function checkRateLimit(identifier: string): boolean {
        const now = Date.now();
        const entry = attempts.get(identifier);
      
        if (!entry || now > entry.resetTime) {
          attempts.set(identifier, { count: 1, resetTime: now + WINDOW_MS });
          return true; // Allowed
        }
      
        if (entry.count >= MAX_ATTEMPTS) {
          return false; // Rate limited
        }
      
        entry.count++;
        return true; // Allowed
      }
      ```
      
      ```typescript
      // auth.ts
      import { checkRateLimit } from "@/lib/rate-limit";
      
      Credentials({
        async authorize(credentials) {
          const email = credentials.email as string;
      
          // Rate limit by email address
          if (!checkRateLimit(email)) {
            return null; // Silently reject (don't reveal rate limiting)
          }
      
          // ... normal auth logic
        },
      });
      ```
      
      **Why good:** Named constants for window and max attempts, per-email rate limiting, silent rejection (doesn't tell attacker they're rate limited), simple in-memory store (use Redis in production)
      
      ---
      
      _See [core.md](core.md) for basic configuration and [session.md](session.md) for session strategies._
      
    • session.md 5.6 KB
      # Session Strategies & Access
      
      > Code examples for Auth.js session management - JWT vs database sessions, accessing sessions in different contexts. See [SKILL.md](../SKILL.md) for core concepts.
      
      ---
      
      ## Pattern 1: JWT Sessions (Default)
      
      ### Good Example - JWT Configuration
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import GitHub from "next-auth/providers/github";
      
      const MAX_AGE_SECONDS = 30 * 24 * 60 * 60; // 30 days
      const UPDATE_AGE_SECONDS = 24 * 60 * 60; // 24 hours
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        providers: [GitHub],
        session: {
          strategy: "jwt",
          maxAge: MAX_AGE_SECONDS,
          updateAge: UPDATE_AGE_SECONDS, // How often to refresh the JWT
        },
      });
      ```
      
      **Why good:** Named constants for time values, explicit strategy declaration, `updateAge` prevents unnecessary JWT refreshes
      
      ---
      
      ## Pattern 2: Database Sessions
      
      ### Good Example - Database Session with Prisma
      
      ```typescript
      // auth.ts
      import NextAuth from "next-auth";
      import GitHub from "next-auth/providers/github";
      import { PrismaAdapter } from "@auth/prisma-adapter";
      import { prisma } from "@/lib/prisma";
      
      export const { auth, handlers, signIn, signOut } = NextAuth({
        adapter: PrismaAdapter(prisma),
        providers: [GitHub],
        session: {
          strategy: "database", // Explicit (default when adapter is present)
        },
      });
      ```
      
      See [database.md](database.md) for the required Prisma/Drizzle schema models (User, Account, Session, VerificationToken).
      
      **Why good:** PrismaAdapter handles session CRUD, database sessions support immediate revocation, explicit `strategy: "database"` when adapter is present
      
      ### Bad Example - Database Strategy Without Adapter
      
      ```typescript
      // BAD: Database strategy requires an adapter
      export const { auth } = NextAuth({
        providers: [GitHub],
        session: {
          strategy: "database", // Will fail - no adapter configured
        },
      });
      ```
      
      **Why bad:** Database strategy requires a database adapter; will throw runtime error
      
      ---
      
      ## Pattern 3: Accessing Sessions in Different Contexts
      
      ### Good Example - Server Component
      
      ```typescript
      // app/profile/page.tsx
      import { auth } from "@/auth";
      import { redirect } from "next/navigation";
      
      export default async function ProfilePage() {
        const session = await auth();
      
        if (!session?.user) {
          redirect("/login");
        }
      
        return (
          <div>
            <h1>Profile</h1>
            <p>Name: {session.user.name}</p>
            <p>Email: {session.user.email}</p>
            <p>Role: {session.user.role}</p>
          </div>
        );
      }
      ```
      
      ### Good Example - Route Handler
      
      ```typescript
      // app/api/profile/route.ts
      import { auth } from "@/auth";
      import { NextResponse } from "next/server";
      
      export const GET = auth(function GET(req) {
        if (!req.auth?.user) {
          return NextResponse.json(
            { error: "Authentication required" },
            { status: 401 },
          );
        }
      
        return NextResponse.json({
          id: req.auth.user.id,
          name: req.auth.user.name,
          role: req.auth.user.role,
        });
      });
      ```
      
      ### Good Example - Server Action
      
      ```typescript
      // app/actions.ts
      "use server";
      
      import { auth } from "@/auth";
      
      export async function updateProfile(formData: FormData) {
        const session = await auth();
      
        if (!session?.user) {
          throw new Error("Unauthorized");
        }
      
        const name = formData.get("name") as string;
      
        await db.user.update({
          where: { id: session.user.id },
          data: { name },
        });
      
        return { success: true };
      }
      ```
      
      ### Good Example - Client Component with SessionProvider
      
      ```typescript
      // components/session-info.tsx
      "use client";
      
      import { useSession } from "next-auth/react";
      
      export function SessionInfo() {
        const { data: session, status } = useSession();
      
        if (status === "loading") {
          return <p>Loading session...</p>;
        }
      
        if (status === "unauthenticated") {
          return <p>Not signed in</p>;
        }
      
        return (
          <div>
            <p>Signed in as {session?.user?.name}</p>
            <p>Role: {session?.user?.role}</p>
          </div>
        );
      }
      ```
      
      **Why good:** Each context uses the appropriate method, server-side uses `auth()` directly, client-side uses `useSession()` with loading states
      
      ### Bad Example - Using Deprecated Functions
      
      ```typescript
      // BAD: v4 patterns
      import { getServerSession } from "next-auth"; // Deprecated
      import { authOptions } from "@/app/api/auth/[...nextauth]/route"; // Deprecated pattern
      
      export default async function Page() {
        const session = await getServerSession(authOptions); // BAD
        // ...
      }
      ```
      
      **Why bad:** `getServerSession` is deprecated in v5, `authOptions` export pattern is v4, should use `auth()` from `@/auth`
      
      ---
      
      ## Pattern 4: Session Update (Refreshing Client Session)
      
      ### Good Example - Updating Session After Profile Change
      
      ```typescript
      // components/profile-form.tsx
      "use client";
      
      import { useSession } from "next-auth/react";
      import { useState } from "react";
      
      export function ProfileForm() {
        const { data: session, update } = useSession();
        const [name, setName] = useState(session?.user?.name ?? "");
      
        const handleSubmit = async (e: React.FormEvent) => {
          e.preventDefault();
      
          // Update in database via Server Action
          await updateProfile(new FormData(e.target as HTMLFormElement));
      
          // Refresh the client session to reflect changes
          await update({ name });
        };
      
        return (
          <form onSubmit={handleSubmit}>
            <input
              name="name"
              value={name}
              onChange={(e) => setName(e.target.value)}
            />
            <button type="submit">Save</button>
          </form>
        );
      }
      ```
      
      **Why good:** `update()` refreshes client session after server-side change, no full page reload needed, optimistic UI with local state
      
      ---
      
      _See [middleware.md](middleware.md) for route protection patterns and [database.md](database.md) for adapter setup._
      
  • reference.md 9.4 KB
    # Auth.js (NextAuth v5) Reference
    
    > Decision frameworks, anti-patterns, and red flags for Auth.js development. See [SKILL.md](SKILL.md) for core concepts and [examples/](examples/) for code examples.
    
    ---
    
    ## Decision Framework
    
    ### Session Strategy Selection
    
    ```
    Do you need to revoke sessions immediately?
    ├─ YES → Database sessions (adapter required)
    │   └─ Deleting a session row instantly invalidates it
    └─ NO → Do you need Edge runtime support (middleware)?
        ├─ YES → JWT sessions (default, no adapter needed)
        │   └─ Split config: auth.config.ts (Edge) + auth.ts (Node)
        └─ NO → Do you need to store session data server-side?
            ├─ YES → Database sessions
            └─ NO → JWT sessions (simpler, no database dependency)
    ```
    
    ### Provider Selection
    
    ```
    What sign-in method do users need?
    ├─ Social login (Google, GitHub, etc.) → OAuth provider
    │   └─ Auto-detects AUTH_PROVIDER_ID and AUTH_PROVIDER_SECRET
    ├─ Email with magic link → Email provider
    │   └─ Requires database adapter for verification tokens
    ├─ Username/password → Credentials provider
    │   └─ You handle password hashing and validation
    ├─ Passkeys → WebAuthn provider (experimental)
    │   └─ Requires database adapter
    └─ Multiple methods → Combine providers in the array
    ```
    
    ### Where to Check Session
    
    ```
    Where is the code running?
    ├─ Server Component → const session = await auth()
    ├─ Route Handler → export const GET = auth(function GET(req) { req.auth })
    ├─ Server Action → const session = await auth()
    ├─ Middleware/Proxy → export { auth as middleware } from "@/auth"
    ├─ Client Component → useSession() (requires SessionProvider)
    └─ API Route (Pages Router) → const session = await auth(req, res)
    ```
    
    ### Middleware/Proxy vs Per-Page Protection
    
    ```
    What level of protection do you need?
    ├─ Redirect unauthenticated users globally → Middleware/proxy
    │   └─ Use matcher to exclude public routes
    ├─ Different behavior per page → Per-page auth() checks
    │   └─ More granular, handles authorization too
    ├─ Role-based access → Per-page checks (middleware can't easily do RBAC)
    └─ Both → Middleware for basic auth + per-page for authorization
    ```
    
    ### Database Adapter Selection
    
    ```
    What ORM/database are you using?
    ├─ Prisma → @auth/prisma-adapter
    ├─ Drizzle → @auth/drizzle-adapter
    ├─ MongoDB → @auth/mongodb-adapter
    ├─ Supabase → @auth/supabase-adapter
    ├─ Firebase → @auth/firebase-adapter
    ├─ Other → Check authjs.dev/reference/core/adapters
    └─ None → No adapter needed (JWT sessions work without database)
    ```
    
    ---
    
    ## RED FLAGS
    
    ### High Priority Issues
    
    - **Using `getServerSession(authOptions)`** - Deprecated in v5; use `auth()` from your `auth.ts` export
    - **Using `NEXTAUTH_SECRET`** - Deprecated; use `AUTH_SECRET`
    - **Using `NEXTAUTH_URL`** - Usually unnecessary in v5; auto-detected in most deployments
    - **Missing `AUTH_SECRET` in production** - Auth will fail silently or throw cryptic errors
    - **Middleware/proxy as sole authorization** - Runs before rendering but doesn't replace per-route authorization checks
    - **Credentials provider without rate limiting** - Vulnerable to brute-force attacks
    - **Storing passwords in plain text** - Always hash with bcrypt/argon2 in the `authorize` function
    
    ### Medium Priority Issues
    
    - **Not splitting config for Edge** - Database adapters don't work on Edge runtime; split into `auth.config.ts` + `auth.ts`
    - **Using `useSession()` without `SessionProvider`** - Returns undefined, fails silently
    - **Exposing access tokens to client** - Only put necessary data in the session callback
    - **Not customizing the redirect callback** - Default behavior may allow open redirect vulnerabilities
    - **Missing TypeScript type extensions** - Custom session fields are untyped without declaration merging
    - **Using `session.user.id` without JWT callback** - `id` is not in the default JWT; must be added in `jwt` callback
    
    ### Common Mistakes
    
    - **Importing `signIn`/`signOut` from wrong package** - Server: `import { signIn } from "@/auth"`; Client: `import { signIn } from "next-auth/react"`
    - **Calling `auth()` in Client Components** - `auth()` is server-only; use `useSession()` in Client Components
    - **Not handling the `user` parameter in JWT callback** - `user` is only available on first sign-in; check for it before accessing
    - **Forgetting `authorized` callback returns boolean** - Return `true` to allow, `false` to deny, or `Response` for redirect
    - **Cookie name change from v4** - Cookies changed from `next-auth.*` to `authjs.*` prefix
    - **Wrapping `signIn()` in try/catch expecting a return value** - `signIn()` throws a NEXT_REDIRECT internally; call it directly in a form action, not inside try/catch
    
    ### Gotchas & Edge Cases
    
    - **`signIn()` throws a NEXT_REDIRECT exception** - Don't wrap in try/catch expecting a return value; it redirects internally
    - **JWT callback runs on EVERY `auth()` call** - Keep it lightweight; don't make database queries here
    - **`user` parameter in JWT callback is only present at sign-in** - Subsequent calls only have `token`
    - **Credentials provider doesn't support database sessions by default** - Use JWT strategy or implement custom session management
    - **OAuth providers auto-detect env vars** - `AUTH_GITHUB_ID` is found automatically; explicit `clientId` overrides it
    - **Session expiry differs between strategies** - JWT: `maxAge` on the cookie (default 30 days); Database: `expires` column on session row
    - **`redirect` callback fires on both sign-in and sign-out** - Handle both cases
    - **Middleware/proxy runs on every request matching the matcher** - Keep it fast; avoid database calls in middleware (Next.js 14/15 Edge)
    - **`auth()` in middleware uses JWT only** - Even with database sessions, middleware reads the JWT (it can't access the database on Edge). Next.js 16 proxy runs on Node.js and can access the database.
    - **Account linking happens automatically** - Users with the same email across providers are linked; control via `allowDangerousEmailAccountLinking`
    
    ---
    
    ## v4 to v5 Migration Quick Reference
    
    | v4 Pattern                         | v5 Replacement                                                       |
    | ---------------------------------- | -------------------------------------------------------------------- |
    | `pages/api/auth/[...nextauth].ts`  | `app/api/auth/[...nextauth]/route.ts`                                |
    | `export const authOptions = {...}` | `export const { auth, handlers, signIn, signOut } = NextAuth({...})` |
    | `getServerSession(authOptions)`    | `auth()`                                                             |
    | `getSession()`                     | `auth()`                                                             |
    | `getToken()`                       | `auth()` (access token via jwt callback)                             |
    | `useSession()` (client)            | `useSession()` (unchanged, still needs SessionProvider)              |
    | `withAuth()` middleware            | `export { auth as middleware }` or authorized callback               |
    | `NEXTAUTH_SECRET`                  | `AUTH_SECRET`                                                        |
    | `NEXTAUTH_URL`                     | Usually auto-detected (optional)                                     |
    | `next-auth/next` import            | Deprecated - import from `next-auth` or `@/auth`                     |
    | `next-auth/middleware` import      | Deprecated - export from `@/auth`                                    |
    | `middleware.ts` (Next.js 16+)      | Renamed to `proxy.ts` - same Auth.js integration pattern             |
    | `@next-auth/prisma-adapter`        | `@auth/prisma-adapter`                                               |
    | `NextAuthOptions` type             | `NextAuthConfig` type                                                |
    
    ---
    
    ## Quick Reference
    
    ### Configuration Checklist
    
    - [ ] `auth.ts` at project root with `NextAuth()` export
    - [ ] `app/api/auth/[...nextauth]/route.ts` with `handlers` export
    - [ ] `AUTH_SECRET` set in environment (generate with `npx auth secret`)
    - [ ] Provider env vars use `AUTH_` prefix
    - [ ] TypeScript types extended in `types/next-auth.d.ts`
    - [ ] `SessionProvider` wrapping app for client-side session access
    
    ### Security Checklist
    
    - [ ] `AUTH_SECRET` is a strong, randomly generated value
    - [ ] Credentials provider uses password hashing (bcrypt/argon2)
    - [ ] Rate limiting on sign-in endpoints
    - [ ] `redirect` callback validates URLs (prevents open redirects)
    - [ ] Session data doesn't expose sensitive tokens to client
    - [ ] Server Actions check `auth()` for authorization (not just middleware)
    - [ ] `signIn` callback validates allowed users/domains
    
    ### Session Strategy Comparison
    
    | Feature            | JWT (default)            | Database                  |
    | ------------------ | ------------------------ | ------------------------- |
    | Setup complexity   | None (no adapter)        | Requires adapter + DB     |
    | Edge compatible    | Yes                      | No (needs Node runtime)   |
    | Session revocation | Not immediate            | Immediate (delete row)    |
    | Storage            | Encrypted cookie         | Database table            |
    | Scalability        | Stateless, scales easily | Depends on DB             |
    | Custom data        | Via jwt callback         | Via session table columns |
    | Default expiry     | 30 days (cookie maxAge)  | 30 days (session.expires) |
    
  • SKILL.md 12.6 KB
    ---
    name: api-auth-nextauth
    description: Auth.js (NextAuth v5) authentication patterns - configuration, providers, session strategies, middleware, database adapters, role-based access, Edge compatibility
    ---
    
    # Auth.js (NextAuth v5) Patterns
    
    > **Quick Guide:** Configure Auth.js in a root `auth.ts` file exporting `{ auth, handlers, signIn, signOut }` from `NextAuth()`. Use the unified `auth()` function everywhere (Server Components, Route Handlers, middleware). Default session strategy is JWT (cookie-based); add a database adapter for persistent sessions. Protect routes via middleware or per-page `auth()` checks.
    
    ---
    
    <critical_requirements>
    
    ## CRITICAL: Before Using This Skill
    
    > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants)
    
    **(You MUST configure Auth.js in a root `auth.ts` file and export `{ auth, handlers, signIn, signOut }` from `NextAuth()`)**
    
    **(You MUST use the unified `auth()` function for server-side session access - NOT the deprecated `getServerSession()`, `getSession()`, or `getToken()`)**
    
    **(You MUST use `AUTH_SECRET` environment variable - `NEXTAUTH_SECRET` is deprecated in v5)**
    
    **(You MUST use `AUTH_` prefixed environment variables for provider credentials (e.g., `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`) - they are auto-detected)**
    
    **(You MUST split auth config into `auth.config.ts` (Edge-compatible) and `auth.ts` (with adapter) when using database sessions with middleware)**
    
    **(You MUST check session inside Server Actions and API routes - middleware alone is NOT sufficient for authorization)**
    
    </critical_requirements>
    
    ---
    
    **Auto-detection:** Auth.js, NextAuth, next-auth, authjs, auth.ts, auth.config.ts, NextAuth(), signIn, signOut, auth(), handlers, SessionProvider, useSession, AUTH_SECRET, OAuth provider, credentials provider, database adapter, @auth/prisma-adapter, @auth/drizzle-adapter, authorized callback, jwt callback, session callback, proxy auth, middleware auth
    
    **When to use:**
    
    - Adding authentication to Next.js, SvelteKit, Express, or Qwik apps
    - Implementing OAuth login (GitHub, Google, Discord, etc.) with 80+ built-in providers
    - Building email/magic link authentication flows
    - Need JWT or database-backed session management
    - Projects requiring Edge-compatible middleware authentication
    
    **When NOT to use:**
    
    - Building a custom auth system from scratch (Auth.js is opinionated)
    - Need fine-grained organization/team management out of the box
    - Mobile-only apps without web frontend
    - Need self-hosted auth with plugin architecture
    
    **Key patterns covered:**
    
    - Auth configuration (`auth.ts`, `auth.config.ts`)
    - OAuth providers (GitHub, Google, Credentials, Email)
    - Session strategies (JWT vs database)
    - Session access (Server Components, Route Handlers, Client Components)
    - Middleware/proxy route protection
    - Database adapters (Prisma, Drizzle)
    - Callbacks (jwt, session, signIn, redirect)
    - Role-based access control
    - Edge compatibility split configuration
    
    **Detailed Resources:**
    
    - For decision frameworks and anti-patterns, see [reference.md](reference.md)
    
    **Core patterns:**
    
    - [examples/core.md](examples/core.md) - Auth configuration, providers, callbacks
    - [examples/session.md](examples/session.md) - Session strategies, session access patterns
    - [examples/middleware.md](examples/middleware.md) - Route protection, middleware, Edge compatibility
    - [examples/database.md](examples/database.md) - Database adapters, Prisma, Drizzle
    - [examples/patterns.md](examples/patterns.md) - Role-based access, magic links, account linking
    
    ---
    
    <philosophy>
    
    ## Philosophy
    
    Auth.js (v5) consolidates authentication into a **single, unified API**. The `auth()` function replaces `getServerSession`, `getSession`, `withAuth`, and `getToken` from v4 for server-side use. `useSession()` remains the correct client-side API. Configuration lives in a root file, not in API routes.
    
    **Core principles:**
    
    1. **Framework-agnostic** - Works with Next.js, SvelteKit, Express, Qwik
    2. **Unified API** - Single `auth()` function for all server-side contexts
    3. **Provider ecosystem** - 80+ built-in OAuth providers with auto-detection of `AUTH_*` env vars
    4. **JWT by default** - Stateless sessions in encrypted cookies, no database required
    5. **Edge-compatible** - Middleware runs on Edge runtime with split configuration (Next.js 16 proxy runs on Node.js)
    6. **Progressive complexity** - Start with OAuth, add database adapter, then customize callbacks
    
    </philosophy>
    
    ---
    
    <patterns>
    
    ## Core Patterns
    
    ### Pattern 1: Auth Configuration
    
    The central configuration file exports everything you need from `NextAuth()`.
    
    #### Basic OAuth Setup
    
    ```typescript
    // auth.ts
    import NextAuth from "next-auth";
    import GitHub from "next-auth/providers/github";
    import Google from "next-auth/providers/google";
    
    export const { auth, handlers, signIn, signOut } = NextAuth({
      providers: [
        GitHub, // Auto-detects AUTH_GITHUB_ID and AUTH_GITHUB_SECRET
        Google, // Auto-detects AUTH_GOOGLE_ID, AUTH_GOOGLE_SECRET
      ],
    });
    ```
    
    ```typescript
    // app/api/auth/[...nextauth]/route.ts
    import { handlers } from "@/auth";
    
    export const { GET, POST } = handlers;
    ```
    
    **Why good:** Single config file exports all auth utilities, providers auto-detect `AUTH_*` env vars, API route is minimal
    
    #### Environment Variables
    
    ```bash
    # .env.local
    AUTH_SECRET="generate-with-npx-auth-secret"  # Required
    AUTH_GITHUB_ID="your-github-client-id"       # Auto-detected by GitHub provider
    AUTH_GITHUB_SECRET="your-github-secret"      # Auto-detected by GitHub provider
    AUTH_GOOGLE_ID="your-google-id"       # Auto-detected by Google provider
    AUTH_GOOGLE_SECRET="your-google-secret"
    ```
    
    **Why good:** `AUTH_` prefix is standardized in v5, `AUTH_SECRET` replaces deprecated `NEXTAUTH_SECRET`, providers auto-detect credentials
    
    ---
    
    ### Pattern 2: Providers
    
    Auth.js supports OAuth, email/magic link, and credentials authentication. 80+ built-in OAuth providers auto-detect `AUTH_*` env vars.
    
    ```typescript
    // OAuth: customize profile mapping
    GitHub({
      profile(profile) {
        return {
          id: String(profile.id),
          name: profile.name ?? profile.login,
          role: "user",
        };
      },
    });
    
    // Credentials: validate input, return null on failure
    Credentials({
      async authorize(credentials) {
        const parsed = LoginSchema.safeParse(credentials);
        if (!parsed.success) return null;
        const user = await getUserByEmail(parsed.data.email);
        if (
          !user ||
          !(await verifyPassword(parsed.data.password, user.hashedPassword))
        )
          return null;
        return { id: user.id, name: user.name, email: user.email };
      },
    });
    ```
    
    **Key rules:** Validate input before DB lookup, always hash passwords, return `null` on failure (never throw - it leaks info). See [examples/core.md](examples/core.md) for complete implementations.
    
    ---
    
    ### Pattern 3: Callbacks
    
    Four callbacks customize auth behavior. Data flows: **jwt callback** (enrich token) -> **session callback** (expose to client).
    
    ```typescript
    callbacks: {
      jwt({ token, user }) {
        if (user) { token.id = user.id; token.role = user.role ?? "user"; }
        return token;
      },
      session({ session, token }) {
        session.user.id = token.id as string;
        session.user.role = token.role as string;
        return session;
      },
    }
    ```
    
    **Key rules:** JWT callback runs on EVERY `auth()` call (keep lightweight), `user` param is only present at sign-in, never expose OAuth tokens to client. See [examples/core.md](examples/core.md) for complete callback implementations including `signIn` and `redirect`.
    
    ---
    
    ### Pattern 4: Session Access
    
    The unified `auth()` function replaces `getServerSession`, `getSession`, and `getToken` from v4 for server-side use. `useSession()` remains for Client Components.
    
    | Context          | How to access session                                     |
    | ---------------- | --------------------------------------------------------- |
    | Server Component | `const session = await auth()`                            |
    | Route Handler    | `export const GET = auth(function GET(req) { req.auth })` |
    | Server Action    | `const session = await auth()`                            |
    | Middleware/Proxy | `export { auth as middleware }` or `authorized` callback  |
    | Client Component | `useSession()` (requires `SessionProvider` in layout)     |
    
    **Key rules:** Server-side imports come from `@/auth`, client-side imports from `next-auth/react`. Never call `auth()` in Client Components. See [examples/session.md](examples/session.md) for complete implementations.
    
    ---
    
    ### Pattern 5: Sign In / Sign Out Actions
    
    Two approaches: Server Actions (recommended, progressive enhancement) or client-side.
    
    ```typescript
    // Server-side (recommended): import from @/auth, use Server Actions in forms
    import { signIn, signOut } from "@/auth";
    // In form action: await signIn("github", { redirectTo: "/dashboard" })
    // In form action: await signOut({ redirectTo: "/" })
    
    // Client-side: import from next-auth/react, use onClick handlers
    import { signIn, signOut } from "next-auth/react";
    // onClick: signIn("github", { callbackUrl: "/dashboard" })
    ```
    
    **Key rules:** Server-side uses `redirectTo`, client-side uses `callbackUrl`. `signIn()` throws a NEXT_REDIRECT exception internally -- don't wrap in try/catch expecting a return value. See [examples/core.md](examples/core.md) for complete implementations.
    
    ---
    
    ### Pattern 6: TypeScript Extensions
    
    Extend session and JWT types via declaration merging in `types/next-auth.d.ts`. Declare custom fields (e.g., `id`, `role`) on `Session`, `User`, and `JWT` interfaces using `& DefaultSession["user"]` to preserve defaults. See [examples/core.md](examples/core.md) for the complete type declaration example.
    
    </patterns>
    
    ---
    
    <integration>
    
    ## Integration Guide
    
    **Auth.js is the authentication layer.** It handles identity verification, session management, and route protection. It does NOT handle authorization logic (role checks, permission systems) -- that is application code.
    
    **Framework support:** Auth.js works with multiple web frameworks via framework-specific packages (`next-auth`, `@auth/sveltekit`, `@auth/express`).
    
    **Database adapters:** For database sessions, Auth.js provides adapter packages (`@auth/prisma-adapter`, `@auth/drizzle-adapter`, etc.) that integrate with your ORM. See [examples/database.md](examples/database.md).
    
    **Session strategy depends on your needs:**
    
    - **JWT (default)** - No database needed, works on Edge, stateless
    - **Database** - Requires adapter, server-side session store, supports immediate revocation
    
    **Auth.js does NOT handle:** fine-grained authorization/RBAC, rate limiting, or database queries beyond auth -- those are application-level concerns.
    
    </integration>
    
    ---
    
    <red_flags>
    
    ## RED FLAGS
    
    - **Using `getServerSession(authOptions)`** -- deprecated in v5; use `auth()` from your `auth.ts`
    - **Using `NEXTAUTH_SECRET` or `NEXTAUTH_URL`** -- deprecated; use `AUTH_SECRET` (URL is auto-detected)
    - **Credentials provider without rate limiting** -- vulnerable to brute-force attacks
    - **Exposing OAuth tokens to client via session callback** -- keep `accessToken`/`refreshToken` server-side only
    - **Middleware/proxy as sole authorization** -- runs before rendering but does not replace per-route checks in Server Actions/API routes
    - **Database adapter imported in middleware** -- database ORMs can't run on Edge runtime (Next.js 14/15); split config into `auth.config.ts` + `auth.ts`
    - **Wrapping `signIn()` in try/catch** -- it throws a NEXT_REDIRECT exception internally (this is intentional)
    - **JWT callback querying database on every call** -- runs on EVERY `auth()` invocation; keep it lightweight
    
    See [reference.md](reference.md) for the complete anti-pattern list, gotchas, and migration table.
    
    </red_flags>
    
    ---
    
    <critical_reminders>
    
    ## CRITICAL REMINDERS
    
    > **All code must follow project conventions in CLAUDE.md**
    
    **(You MUST configure Auth.js in a root `auth.ts` file and export `{ auth, handlers, signIn, signOut }` from `NextAuth()`)**
    
    **(You MUST use the unified `auth()` function for server-side session access - NOT the deprecated `getServerSession()`, `getSession()`, or `getToken()`)**
    
    **(You MUST use `AUTH_SECRET` environment variable - `NEXTAUTH_SECRET` is deprecated in v5)**
    
    **(You MUST use `AUTH_` prefixed environment variables for provider credentials (e.g., `AUTH_GITHUB_ID`, `AUTH_GITHUB_SECRET`) - they are auto-detected)**
    
    **(You MUST split auth config into `auth.config.ts` (Edge-compatible) and `auth.ts` (with adapter) when using database sessions with middleware)**
    
    **(You MUST check session inside Server Actions and API routes - middleware alone is NOT sufficient for authorization)**
    
    **Failure to follow these rules will cause authentication failures, expose deprecated patterns, or create security vulnerabilities.**
    
    </critical_reminders>
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related