api-auth-nextauth
Auth.js (NextAuth v5) authentication patterns - configuration, providers, session strategies, middleware, database adapters, role-based access, Edge compatibility
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/api-auth-nextauth/skills/api-auth-nextauth
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
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.tsfile exporting{ auth, handlers, signIn, signOut }fromNextAuth(). Use the unifiedauth()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-pageauth()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:
- examples/core.md - Auth configuration, providers, callbacks
- examples/session.md - Session strategies, session access patterns
- examples/middleware.md - Route protection, middleware, Edge compatibility
- examples/database.md - Database adapters, Prisma, Drizzle
- examples/patterns.md - Role-based access, magic links, account linking
<red_flags>
RED FLAGS
- Using
getServerSession(authOptions)-- deprecated in v5; useauth()from yourauth.ts - Using
NEXTAUTH_SECRETorNEXTAUTH_URL-- deprecated; useAUTH_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/refreshTokenserver-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.
Reviews (0)
No reviews yet.
No comments yet.