Provides authentication implementation patterns for Next.js 15+ App Router using Auth.js 5 (NextAuth.js). Use when setting up authentication flows,…
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:
npm install next-auth@beta
2. Configure Environment Variables
Create .env.local with required variables:
# 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:
openssl rand -base64 32
3. Create Auth Configuration
Create auth.ts in the project root with providers and callbacks:
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:
export { GET, POST } from "@/auth";
5. Add Middleware for Route Protection
Create middleware.ts in the project root:
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:
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session) {
redirect("/login");
}
return (
<div>
<h1>Welcome, {session.user.name}</h1>
</div>
);
}
7. Secure Server Actions
Always verify authentication in Server Actions before mutations:
"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:
// 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 (
<form action={handleLogin}>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit">Sign In</button>
</form>
);
}
For client-side sign-out:
"use client";
import { signOut } from "next-auth/react";
export function SignOutButton() {
return <button onClick={() => signOut()}>Sign Out</button>;
}
9. Implement Role-Based Access
Check roles in Server Components:
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 <AdminDashboard />;
}
10. Extend TypeScript Types
Create types/next-auth.d.ts for type-safe sessions:
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:
// 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 (
<main>
<h1>Welcome, {session.user.name}</h1>
<p>Email: {session.user.email}</p>
<TodoList todos={todos} />
</main>
);
}
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:
// 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 (
<main>
<h1>Admin Panel</h1>
<p>Welcome, administrator {session.user.name}</p>
</main>
);
}
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:
// 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 (
<form action={createTodo}>
<input name="title" placeholder="New todo..." required />
<button type="submit">Add Todo</button>
</form>
);
}
Output: Todo created only for authenticated user; unauthorized requests throw error.
Best Practices
Use Server Components by default - Access session directly without client-side JavaScript
Minimize Client Components - Only use useSession() for reactive session updates
Cache session checks - Use React's cache() for repeated lookups in the same render
Middleware for optimistic checks - Redirect quickly, but always re-verify in Server Actions
Treat Server Actions like API endpoints - Always authenticate before mutations
Never hardcode secrets - Use environment variables for all credentials
Implement proper error handling - Return appropriate HTTP status codes
Use TypeScript type extensions - Extend NextAuth types for custom fields
Separate auth logic - Create a DAL (Data Access Layer) for consistent checks
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
// ❌ 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");
}
// ❌ 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 - Complete Auth.js 5 setup guide with Prisma/Drizzle adapters
references/oauth-providers.md - Provider-specific configurations (GitHub, Google, Discord, Auth0, etc.)
references/database-adapter.md - Database session management with Prisma, Drizzle, and custom adapters
references/testing-patterns.md - Testing authentication flows with Vitest and Playwrightdon't have the plugin yet? install it then click "run inline in claude" again.
by @mindrally