---
name: nextjs-authentication
description: Provides authentication implementation patterns for Next.js 15+ App Router using Auth.js 5 (NextAuth.js). Use when setting up authentication flows, implementing protected routes, managing sessions in Server Components and Server Actions, configuring OAuth providers, implementing role-based access control, or handling sign-in/sign-out flows in Next.js applications.
allowed-tools: Read, Write, Edit, Bash
---
# Next.js Authentication
## Overview
Provides authentication implementation patterns for Next.js 15+ App Router using Auth.js 5 (NextAuth.js), covering the complete authentication lifecycle from initial setup to production-ready role-based access control implementations.
## When to Use
- Setting up Auth.js 5 from scratch or adding OAuth providers
- Implementing protected routes with Middleware
- Handling authentication in Server Components and Server Actions
- Implementing role-based access control (RBAC)
- Creating credential-based or OAuth sign-in/sign-out flows
## Instructions
### 1. Install Dependencies
Install Auth.js v5 (beta) for Next.js App Router:
```bash
npm install next-auth@beta
```
### 2. Configure Environment Variables
Create `.env.local` with required variables:
```bash
# Required for Auth.js
AUTH_SECRET="your-secret-key-here"
AUTH_URL="http://localhost:3000"
# OAuth Providers (add as needed)
GITHUB_ID="your-github-client-id"
GITHUB_SECRET="your-github-client-secret"
GOOGLE_CLIENT_ID="your-google-client-id"
GOOGLE_CLIENT_SECRET="your-google-client-secret"
```
Generate `AUTH_SECRET` with:
```bash
openssl rand -base64 32
```
### 3. Create Auth Configuration
Create `auth.ts` in the project root with providers and callbacks:
```typescript
import NextAuth from "next-auth";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
callbacks: {
async jwt({ token, user }) {
if (user) {
token.id = user.id;
}
return token;
},
async session({ session, token }) {
if (token) {
session.user.id = token.id as string;
}
return session;
},
},
pages: {
signIn: "/login",
error: "/error",
},
});
```
### 4. Create API Route Handler
Create `app/api/auth/[...nextauth]/route.ts`:
```typescript
export { GET, POST } from "@/auth";
```
### 5. Add Middleware for Route Protection
Create `middleware.ts` in the project root:
```typescript
import { auth } from "@/auth";
import { NextResponse } from "next/server";
export default auth((req) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
const isApiAuthRoute = nextUrl.pathname.startsWith("/api/auth");
const isPublicRoute = ["/", "/login", "/register"].includes(nextUrl.pathname);
const isProtectedRoute = nextUrl.pathname.startsWith("/dashboard");
if (isApiAuthRoute) return NextResponse.next();
if (!isLoggedIn && isProtectedRoute) {
return NextResponse.redirect(new URL("/login", nextUrl));
}
if (isLoggedIn && nextUrl.pathname === "/login") {
return NextResponse.redirect(new URL("/dashboard", nextUrl));
}
return NextResponse.next();
});
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico|.*\\.png$).*)"],
};
```
### 6. Access Session in Server Components
Use the `auth()` function to access session in Server Components:
```tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect("/login");
}
return (
Welcome, {session.user.name}
);
}
```
### 7. Secure Server Actions
Always verify authentication in Server Actions before mutations:
```tsx
"use server";
import { auth } from "@/auth";
export async function createTodo(formData: FormData) {
const session = await auth();
if (!session?.user) {
throw new Error("Unauthorized");
}
// Proceed with protected action
const title = formData.get("title") as string;
await db.todo.create({
data: { title, userId: session.user.id },
});
}
```
### 8. Handle Sign-In/Sign-Out
Create a login page with server action:
```tsx
// app/login/page.tsx
import { signIn } from "@/auth";
import { redirect } from "next/navigation";
export default function LoginPage() {
async function handleLogin(formData: FormData) {
"use server";
const result = await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirect: false,
});
if (result?.error) {
return { error: "Invalid credentials" };
}
redirect("/dashboard");
}
return (
);
}
```
For client-side sign-out:
```tsx
"use client";
import { signOut } from "next-auth/react";
export function SignOutButton() {
return ;
}
```
### 9. Implement Role-Based Access
Check roles in Server Components:
```tsx
import { auth } from "@/auth";
import { unauthorized } from "next/navigation";
export default async function AdminPage() {
const session = await auth();
if (session?.user?.role !== "admin") {
unauthorized();
}
return ;
}
```
### 10. Extend TypeScript Types
Create `types/next-auth.d.ts` for type-safe sessions:
```typescript
import { DefaultSession } from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: "user" | "admin";
} & DefaultSession["user"];
}
interface User {
role?: "user" | "admin";
}
}
declare module "next-auth/jwt" {
interface JWT {
id?: string;
role?: "user" | "admin";
}
}
```
## Examples
### Example 1: Complete Protected Dashboard
**Input:** User needs a dashboard accessible only to authenticated users
**Implementation:**
```tsx
// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
import { getUserTodos } from "@/app/lib/data";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user?.id) {
redirect("/login");
}
const todos = await getUserTodos(session.user.id);
return (
Welcome, {session.user.name}
Email: {session.user.email}
);
}
```
**Output:** Dashboard renders only for authenticated users, with their specific data.
### Example 2: Role-Based Admin Panel
**Input:** Admin panel should be accessible only to users with "admin" role
**Implementation:**
```tsx
// app/admin/page.tsx
import { auth } from "@/auth";
import { unauthorized } from "next/navigation";
export default async function AdminPage() {
const session = await auth();
if (session?.user?.role !== "admin") {
unauthorized();
}
return (
Admin Panel
Welcome, administrator {session.user.name}
);
}
```
**Output:** Only admin users see the panel; others get 401 error.
### Example 3: Secure Server Action with Form
**Input:** Form submission should only work for authenticated users
**Implementation:**
```tsx
// app/components/create-todo-form.tsx
"use server";
import { auth } from "@/auth";
import { revalidatePath } from "next/cache";
export async function createTodo(formData: FormData) {
const session = await auth();
if (!session?.user?.id) {
throw new Error("Unauthorized");
}
const title = formData.get("title") as string;
await db.todo.create({
data: {
title,
userId: session.user.id,
},
});
revalidatePath("/dashboard");
}
// Usage in component
export function CreateTodoForm() {
return (
);
}
```
**Output:** Todo created only for authenticated user; unauthorized requests throw error.
## Best Practices
1. **Use Server Components by default** - Access session directly without client-side JavaScript
2. **Minimize Client Components** - Only use `useSession()` for reactive session updates
3. **Cache session checks** - Use React's `cache()` for repeated lookups in the same render
4. **Middleware for optimistic checks** - Redirect quickly, but always re-verify in Server Actions
5. **Treat Server Actions like API endpoints** - Always authenticate before mutations
6. **Never hardcode secrets** - Use environment variables for all credentials
7. **Implement proper error handling** - Return appropriate HTTP status codes
8. **Use TypeScript type extensions** - Extend NextAuth types for custom fields
9. **Separate auth logic** - Create a DAL (Data Access Layer) for consistent checks
10. **Test authentication flows** - Mock `auth()` function in unit tests
## Constraints and Warnings
### Critical Limitations
- **Middleware runs on Edge runtime** - Cannot use Node.js APIs like database drivers
- **Server Components cannot set cookies** - Use Server Actions for cookie operations
- **Session callback timing** - Only called on session creation/access, not every request
### Common Mistakes
```tsx
// ❌ WRONG: Setting cookies in Server Component
export default async function Page() {
cookies().set("key", "value"); // Won't work
}
// ✅ CORRECT: Use Server Action
async function setCookieAction() {
"use server";
cookies().set("key", "value");
}
```
```typescript
// ❌ WRONG: Database queries in Middleware
export default auth(async (req) => {
const user = await db.user.findUnique(); // Won't work in Edge
});
// ✅ CORRECT: Use only Edge-compatible APIs
export default auth(async (req) => {
const session = req.auth; // This works
});
```
### Security Considerations
- Always verify authentication in Server Actions - middleware alone is not enough
- Use `unauthorized()` for unauthenticated access, `redirect()` for other cases
- Store sensitive tokens in `httpOnly` cookies
- Validate all user input before processing
- Use HTTPS in production
- Set appropriate cookie `sameSite` attributes
## References
- [references/authjs-setup.md](references/authjs-setup.md) - Complete Auth.js 5 setup guide with Prisma/Drizzle adapters
- [references/oauth-providers.md](references/oauth-providers.md) - Provider-specific configurations (GitHub, Google, Discord, Auth0, etc.)
- [references/database-adapter.md](references/database-adapter.md) - Database session management with Prisma, Drizzle, and custom adapters
- [references/testing-patterns.md](references/testing-patterns.md) - Testing authentication flows with Vitest and Playwright