api-auth-nextauth · diff

git:20260316.5ab4775 to git:20260316.5c62783

93 added, 262 removed. Audit A to A.

---
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 session access - NOT the deprecated `getServerSession()`, `getSession()`, `getToken()`, or `useSession()`)**
**(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, 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 (consider Better Auth)
- Mobile-only apps without web frontend (consider Firebase Auth)
- Need self-hosted auth with plugin architecture (consider Better Auth)
**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`, `getToken`, and `useSession` from v4. 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 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
6. **Progressive complexity** - Start with OAuth, add database adapter, then customize callbacks
**When to use Auth.js:**
- Need quick OAuth/social login setup with many providers
- Building Next.js apps where middleware route protection is important
- Want JWT sessions without managing a session store
- Need magic link / email authentication
- Multi-framework teams (Next.js + SvelteKit sharing auth patterns)
**When NOT to use Auth.js:**
- Need organization/team management out of the box (Better Auth)
- Need stateful sessions with full audit trail by default (Better Auth)
- Building a pure API without web framework (use passport.js or custom)
- Need 2FA / TOTP built-in (Auth.js requires custom implementation; Better Auth has plugins)
</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_CLIENT_ID and AUTH_GOOGLE_CLIENT_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_CLIENT_ID="your-google-id" # Auto-detected by Google provider
AUTH_GOOGLE_CLIENT_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.
-
- #### OAuth Provider with Custom Profile
-
- ```typescript
- // auth.ts
- import NextAuth from "next-auth";
- import GitHub from "next-auth/providers/github";
-
- export const { auth, handlers, signIn, signOut } = NextAuth({
- providers: [
- GitHub({
- // Customize the profile data mapped to the user
- profile(profile) {
- return {
- id: String(profile.id),
- name: profile.name ?? profile.login,
- email: profile.email,
- image: profile.avatar_url,
- role: "user", // Custom field
- };
- },
- }),
- ],
- });
- ```
-
- #### Credentials Provider
+ Auth.js supports OAuth, email/magic link, and credentials authentication. 80+ built-in OAuth providers auto-detect `AUTH_*` env vars.
```typescript
- // auth.ts
- import NextAuth from "next-auth";
- import Credentials from "next-auth/providers/credentials";
- import { z } from "zod";
-
- const LoginSchema = z.object({
- email: z.string().email(),
- password: z.string().min(8),
+ // OAuth: customize profile mapping
+ GitHub({
+ profile(profile) {
+ return {
+ id: String(profile.id),
+ name: profile.name ?? profile.login,
+ role: "user",
+ };
+ },
});
- export const { auth, handlers, signIn, signOut } = NextAuth({
- providers: [
- Credentials({
- credentials: {
- email: { label: "Email", type: "email" },
- password: { label: "Password", type: "password" },
- },
- async authorize(credentials) {
- const parsed = LoginSchema.safeParse(credentials);
- if (!parsed.success) return null;
-
- const user = await getUserByEmail(parsed.data.email);
- if (!user) return null;
-
- const passwordMatch = await verifyPassword(
- parsed.data.password,
- user.hashedPassword,
- );
- if (!passwordMatch) return null;
-
- return {
- id: user.id,
- name: user.name,
- email: user.email,
- };
- },
- }),
- ],
+ // 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 };
+ },
});
```
- **Why good:** Zod validation on credentials, null return signals failed auth, user object returned on success
+ **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
- Callbacks customize auth behavior - extending tokens, sessions, controlling sign-in, and redirects.
-
- #### JWT and Session Callbacks for Custom Data
+ Four callbacks customize auth behavior. Data flows: **jwt callback** (enrich token) -> **session callback** (expose to client).
```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: {
- // Called when JWT is created (sign in) or updated (session access)
- async jwt({ token, user, account }) {
- // On first sign in, user and account are available
- if (user) {
- token.role = user.role ?? "user";
- token.id = user.id;
- }
- if (account) {
- token.accessToken = account.access_token;
- }
- return token;
- },
-
- // Called whenever session is checked - shapes what client sees
- async session({ session, token }) {
- if (token) {
- session.user.id = token.id as string;
- session.user.role = token.role as string;
- }
- return session;
- },
-
- // Control who can sign in
- async signIn({ user, account, profile }) {
- // Example: Only allow users with verified email
- if (account?.provider === "github") {
- return profile?.email_verified === true;
- }
- return true;
- },
-
- // Control redirect after sign in/out
- async redirect({ url, baseUrl }) {
- // Allow relative URLs
- if (url.startsWith("/")) return `${baseUrl}${url}`;
- // Allow same-origin URLs
- if (new URL(url).origin === baseUrl) return url;
- return baseUrl;
- },
+ callbacks: {
+ jwt({ token, user, account }) {
+ // `user` only available on first sign-in — check before accessing
+ if (user) { token.id = user.id; token.role = user.role ?? "user"; }
+ if (account) { token.accessToken = account.access_token; }
+ return token;
},
- });
+ session({ session, token }) {
+ // Expose only what client needs — NEVER expose accessToken
+ session.user.id = token.id as string;
+ session.user.role = token.role as string;
+ return session;
+ },
+ signIn({ user, account }) {
+ // Return false to deny, true to allow
+ if (account?.provider === "google") return user.email?.endsWith("@company.com") ?? false;
+ return true;
+ },
+ redirect({ url, baseUrl }) {
+ // Prevent open redirects
+ if (url.startsWith("/")) return `${baseUrl}${url}`;
+ if (new URL(url).origin === baseUrl) return url;
+ return baseUrl;
+ },
+ }
```
- **Why good:** JWT callback enriches token with custom data, session callback exposes only what the client needs, signIn callback controls access, redirect callback prevents open redirects
+ **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.
---
### Pattern 4: Session Access
- The unified `auth()` function works in every context.
-
- #### Server Component
-
- ```typescript
- // 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("/api/auth/signin");
- }
-
- return (
- <div>
- <h1>Welcome, {session.user.name}</h1>
- <p>Role: {session.user.role}</p>
- </div>
- );
- }
- ```
-
- #### Route Handler
-
- ```typescript
- // app/api/user/route.ts
- import { auth } from "@/auth";
- import { NextResponse } from "next/server";
-
- export const GET = auth(function GET(req) {
- if (!req.auth) {
- return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
- }
-
- return NextResponse.json({ user: req.auth.user });
- });
- ```
-
- #### Client Component
-
- ```typescript
- // app/providers.tsx
- "use client";
-
- import { SessionProvider } from "next-auth/react";
-
- export function Providers({ children }: { children: React.ReactNode }) {
- return <SessionProvider>{children}</SessionProvider>;
- }
- ```
-
- ```typescript
- // app/layout.tsx
- import { Providers } from "./providers";
-
- export default function RootLayout({
- children,
- }: {
- children: React.ReactNode;
- }) {
- return (
- <html lang="en">
- <body>
- <Providers>{children}</Providers>
- </body>
- </html>
- );
- }
- ```
-
- ```typescript
- // components/user-menu.tsx
- "use client";
-
- import { useSession, signIn, signOut } from "next-auth/react";
-
- export function UserMenu() {
- const { data: session, status } = useSession();
-
- if (status === "loading") return <div>Loading...</div>;
-
- if (!session) {
- return <button onClick={() => signIn()}>Sign in</button>;
- }
+ The unified `auth()` function replaces `getServerSession`, `getSession`, `getToken`, and `useSession` from v4.
- return (
- <div>
- <span>{session.user.name}</span>
- <button onClick={() => signOut()}>Sign out</button>
- </div>
- );
- }
- ```
+ | 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 | `export { auth as middleware }` or `authorized` callback |
+ | Client Component | `useSession()` (requires `SessionProvider` in layout) |
- **Why good:** `auth()` works in Server Components without context, `auth(handler)` wraps Route Handlers, `SessionProvider` enables `useSession` in client components
+ **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
- #### Server-Side Sign In (Recommended)
-
- ```typescript
- // components/sign-in-button.tsx
- import { signIn } from "@/auth";
-
- export function SignInButton() {
- return (
- <form
- action={async () => {
- "use server";
- await signIn("github", { redirectTo: "/dashboard" });
- }}
- >
- <button type="submit">Sign in with GitHub</button>
- </form>
- );
- }
- ```
-
- #### Server-Side Sign Out
+ Two approaches: Server Actions (recommended, progressive enhancement) or client-side.
```typescript
- // components/sign-out-button.tsx
- import { signOut } from "@/auth";
+ // 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: "/" })
- export function SignOutButton() {
- return (
- <form
- action={async () => {
- "use server";
- await signOut({ redirectTo: "/" });
- }}
- >
- <button type="submit">Sign out</button>
- </form>
- );
- }
+ // Client-side: import from next-auth/react, use onClick handlers
+ import { signIn, signOut } from "next-auth/react";
+ // onClick: signIn("github", { callbackUrl: "/dashboard" })
```
- **Why good:** Server Actions for sign in/out (progressive enhancement), `redirectTo` controls post-auth destination, works without client-side JavaScript
+ **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
#### Extending Session and JWT Types
```typescript
// types/next-auth.d.ts
import type { DefaultSession, DefaultJWT } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: string;
} & DefaultSession["user"];
}
interface User {
role?: string;
}
}
declare module "next-auth/jwt" {
interface JWT extends DefaultJWT {
id?: string;
role?: string;
accessToken?: string;
}
}
```
**Why good:** Type-safe custom session fields, extends default types instead of replacing, declaration merging for all auth contexts
</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.
+ **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.
- **Works with:**
+ **Framework support:** Auth.js works with multiple web frameworks via framework-specific packages (`next-auth`, `@auth/sveltekit`, `@auth/express`).
- - **Next.js** - Primary framework, full App Router support with Server Actions
- - **SvelteKit** - Via `@auth/sveltekit` package
- - **Express** - Via `@auth/express` package
- - **Prisma** - Via `@auth/prisma-adapter` for database sessions
- - **Drizzle** - Via `@auth/drizzle-adapter` for database sessions
+ **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 stack:**
+ **Session strategy depends on your needs:**
- **JWT (default)** - No database needed, works on Edge, stateless
- - **Database** - Requires adapter, server-side session store, supports revocation
-
- **Defers to:**
+ - **Database** - Requires adapter, server-side session store, supports immediate revocation
- - **Database/ORM** - For user data storage and queries beyond auth
- - **Authorization libraries** - For role/permission systems (CASL, etc.)
- - **Rate limiting** - For brute-force protection on sign-in endpoints
+ **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 as sole authorization** -- middleware 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; 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 session access - NOT the deprecated `getServerSession()`, `getSession()`, `getToken()`, or `useSession()`)**
**(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>