
Nextjs Authentication
- 1.6k installs
- 311 repo stars
- Updated June 22, 2026
- giuseppe-trisciuoglio/developer-kit
nextjs-authentication is an agent skill that provides authentication implementation patterns for next.js 15+ app router using auth.js 5 (nextauth.js). use when setting up authentication flows, implementing protected rout
About
nextjs-authentication is an agent skill from giuseppe-trisciuoglio/developer-kit that 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 . # 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 provide Developers invoke nextjs-authentication during build/backend work for backend & apis tasks. The skill documents triggers, prerequisites, and step-by-step workflows grounded in SKILL.md. Compatible with Claude Code, Cursor, and Codex agent runtimes that load marketplace skills. Review the Security Audits panel on this listing before installing in production environments.
- 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
Nextjs Authentication by the numbers
- 1,550 all-time installs (skills.sh)
- +60 installs in the week ending Jul 28, 2026 (Skillselion tracking)
- Ranked #313 of 4,386 Backend & APIs skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Jul 28, 2026 (Skillselion catalog sync)
nextjs-authentication capabilities & compatibility
- Capabilities
- setting up auth.js 5 from scratch or adding oaut · implementing protected routes with middleware · handling authentication in server components and · implementing role based access control (rbac) · creating credential based or oauth sign in/sign
- Use cases
- orchestration
What nextjs-authentication says it does
- Setting up Auth.js 5 from scratch or adding OAuth providers
- Implementing protected routes with Middleware
- Handling authentication in Server Components and Server Actions
npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill nextjs-authenticationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1.6k |
|---|---|
| repo stars | ★ 311 |
| Security audit | 2 / 3 scanners passed |
| Last updated | June 22, 2026 |
| Repository | giuseppe-trisciuoglio/developer-kit ↗ |
What it does
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
Who is it for?
Developers working on backend & apis during build tasks.
Skip if: Tasks outside Backend & APIs scope described in SKILL.md.
When should I use this skill?
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
What you get
Completed backend & apis workflow aligned with SKILL.md steps.
- auth.ts
- middleware.ts
- Protected route handlers and login page
Files
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@beta2. 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 323. 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
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
// ❌ 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
httpOnlycookies - Validate all user input before processing
- Use HTTPS in production
- Set appropriate cookie
sameSiteattributes
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 Playwright
Auth.js 5 Setup Guide
Complete setup guide for Auth.js 5 with Next.js App Router.
Installation
npm install next-auth@beta
# or specific version
npm install @auth/nextjs@latestProject Structure
my-app/
├── auth.ts # Main auth configuration
├── middleware.ts # Route protection
├── types/
│ └── next-auth.d.ts # TypeScript type extensions
├── app/
│ ├── api/
│ │ └── auth/
│ │ └── [...nextauth]/ # API route handler
│ │ └── route.ts
│ ├── login/
│ │ └── page.tsx # Custom login page
│ ├── dashboard/
│ │ └── page.tsx # Protected page
│ └── layout.tsx # Root layout with SessionProvider
├── components/
│ └── auth/
│ ├── sign-in-button.tsx
│ └── user-avatar.tsx
└── lib/
├── auth.ts # Auth utilities
└── dal.ts # Data Access LayerComplete Configuration
1. TypeScript Types
// types/next-auth.d.ts
import { DefaultSession, DefaultUser } from "next-auth";
import { JWT } from "next-auth/jwt";
declare module "next-auth" {
interface Session {
user: {
id: string;
role: "user" | "admin" | "moderator";
permissions: string[];
} & DefaultSession["user"];
}
interface User extends DefaultUser {
role?: "user" | "admin" | "moderator";
permissions?: string[];
}
}
declare module "next-auth/jwt" {
interface JWT {
id?: string;
role?: "user" | "admin" | "moderator";
permissions?: string[];
}
}2. Auth Configuration
// auth.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
// Providers
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
export const {
handlers: { GET, POST },
auth,
signIn,
signOut,
} = NextAuth({
// Database adapter (optional - for database sessions)
adapter: PrismaAdapter(prisma),
// Session configuration
session: {
strategy: "jwt", // or "database"
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // 24 hours
},
// Cookie configuration
cookies: {
sessionToken: {
name: `__Secure-next-auth.session-token`,
options: {
httpOnly: true,
sameSite: "lax",
path: "/",
secure: process.env.NODE_ENV === "production",
},
},
},
// Authentication providers
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
allowDangerousEmailAccountLinking: true,
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
allowDangerousEmailAccountLinking: true,
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user || !user.password) {
return null;
}
const isPasswordValid = await bcrypt.compare(
credentials.password as string,
user.password
);
if (!isPasswordValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
role: user.role,
};
},
}),
],
// Callbacks for customizing behavior
callbacks: {
async signIn({ user, account, profile }) {
// Allow OAuth sign-ins without restrictions
if (account?.provider !== "credentials") {
return true;
}
// Additional validation for credentials provider
return true;
},
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same origin
else if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},
async jwt({ token, user, account, trigger, session }) {
// Persist user data to token
if (user) {
token.id = user.id;
token.role = user.role;
token.permissions = user.permissions;
}
// Handle session updates
if (trigger === "update" && session) {
token.name = session.name;
token.image = session.image;
}
return token;
},
async session({ session, token }) {
// Send token data to session
if (token) {
session.user.id = token.id as string;
session.user.role = token.role as "user" | "admin" | "moderator";
session.user.permissions = token.permissions as string[];
}
return session;
},
},
// Custom pages
pages: {
signIn: "/login",
signOut: "/logout",
error: "/error", // Error code passed in query string as ?error=
verifyRequest: "/verify-email", // (used for check email message)
newUser: "/welcome", // New users will be directed here on first sign in
},
// Events for logging or side effects
events: {
async signIn(message) {
console.log("User signed in:", message.user.email);
},
async signOut(message) {
console.log("User signed out:", message.token.email);
},
async createUser(message) {
console.log("New user created:", message.user.email);
},
},
// Debug mode (only in development)
debug: process.env.NODE_ENV === "development",
});3. API Route Handler
// app/api/auth/[...nextauth]/route.ts
export { GET, POST } from "@/auth";4. Middleware Configuration
// middleware.ts
import { auth } from "@/auth";
import { NextResponse } from "next/server";
// Define route groups
const publicRoutes = ["/", "/login", "/register", "/forgot-password"];
const authRoutes = ["/login", "/register"];
const protectedRoutes = ["/dashboard", "/profile", "/settings"];
const adminRoutes = ["/admin"];
const apiAuthPrefix = "/api/auth";
export default auth((req) => {
const { nextUrl } = req;
const isLoggedIn = !!req.auth;
const pathname = nextUrl.pathname;
// Allow all API auth routes
if (pathname.startsWith(apiAuthPrefix)) {
return NextResponse.next();
}
// Check if route is public
const isPublicRoute = publicRoutes.some(
(route) => pathname === route || pathname.startsWith(`${route}/`)
);
// Check if route requires authentication
const isProtectedRoute = protectedRoutes.some(
(route) => pathname === route || pathname.startsWith(`${route}/`)
);
// Check if route requires admin role
const isAdminRoute = adminRoutes.some(
(route) => pathname === route || pathname.startsWith(`${route}/`)
);
// Check if it's an auth route (login/register)
const isAuthRoute = authRoutes.includes(pathname);
// Redirect logged-in users away from auth pages
if (isLoggedIn && isAuthRoute) {
return NextResponse.redirect(new URL("/dashboard", nextUrl));
}
// Redirect unauthenticated users to login
if (!isLoggedIn && isProtectedRoute) {
const callbackUrl = encodeURIComponent(pathname);
return NextResponse.redirect(
new URL(`/login?callbackUrl=${callbackUrl}`, nextUrl)
);
}
// Check admin access
if (isAdminRoute && req.auth?.user?.role !== "admin") {
return NextResponse.redirect(new URL("/unauthorized", nextUrl));
}
return NextResponse.next();
});
export const config = {
matcher: [
// Skip static files and images
"/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)",
],
};5. Data Access Layer
// lib/dal.ts
import { auth } from "@/auth";
import { cache } from "react";
import { redirect } from "next/navigation";
/**
* Verify current session - cached for same render
*/
export const verifySession = cache(async () => {
const session = await auth();
if (!session?.user) {
return null;
}
return session;
});
/**
* Require authentication - redirects to login if not authenticated
*/
export async function requireAuth() {
const session = await verifySession();
if (!session) {
redirect("/login");
}
return session;
}
/**
* Require specific role
*/
export async function requireRole(role: string) {
const session = await verifySession();
if (!session) {
redirect("/login");
}
if (session.user.role !== role) {
redirect("/unauthorized");
}
return session;
}
/**
* Get current user with data
*/
export async function getCurrentUser() {
const session = await requireAuth();
const user = await prisma.user.findUnique({
where: { id: session.user.id },
select: {
id: true,
email: true,
name: true,
image: true,
role: true,
createdAt: true,
},
});
if (!user) {
throw new Error("User not found");
}
return user;
}6. Auth Utilities
// lib/auth.ts
import bcrypt from "bcryptjs";
/**
* Hash a password
*/
export async function hashPassword(password: string): Promise<string> {
return bcrypt.hash(password, 12);
}
/**
* Verify a password
*/
export async function verifyPassword(
password: string,
hashedPassword: string
): Promise<boolean> {
return bcrypt.compare(password, hashedPassword);
}
/**
* Generate a secure random token
*/
export function generateToken(length: number = 32): string {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}Environment Variables
# .env.local
# NextAuth.js
AUTH_SECRET="your-secret-key-here" # Generate with: openssl rand -base64 32
AUTH_URL="http://localhost:3000"
# OAuth Providers
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"
# Database (if using database sessions)
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# Email (if using email provider)
SMTP_HOST="smtp.gmail.com"
SMTP_PORT="587"
SMTP_USER="your-email@gmail.com"
SMTP_PASSWORD="your-app-password"
FROM_EMAIL="noreply@yourapp.com"Client Component Setup
For client components that need session access:
// components/providers/session-provider.tsx
"use client";
import { SessionProvider as NextAuthSessionProvider } from "next-auth/react";
import { ReactNode } from "react";
export function SessionProvider({ children }: { children: ReactNode }) {
return <NextAuthSessionProvider>{children}</NextAuthSessionProvider>;
}// app/layout.tsx
import { SessionProvider } from "@/components/providers/session-provider";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
<SessionProvider>{children}</SessionProvider>
</body>
</html>
);
}Best Practices
Comprehensive best practices for Next.js authentication with Auth.js 5.
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
// ❌ 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
httpOnlycookies - Validate all user input before processing
- Use HTTPS in production
- Set appropriate cookie
sameSiteattributes
Database Adapter Configuration
Complete guide for using database sessions with Auth.js 5 and Next.js App Router.
Prisma Setup
1. Install Dependencies
npm install @auth/prisma-adapter prisma @prisma/client
npm install -D prisma2. Prisma Schema
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // or "mysql" | "sqlite"
url = env("DATABASE_URL")
}
// NextAuth.js models
model Account {
id String @id @default(cuid())
userId String @map("user_id")
type String
provider String
providerAccountId String @map("provider_account_id")
refresh_token String? @db.Text
access_token String? @db.Text
expires_at Int?
token_type String?
scope String?
id_token String? @db.Text
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
@@map("accounts")
}
model Session {
id String @id @default(cuid())
sessionToken String @unique @map("session_token")
userId String @map("user_id")
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@map("sessions")
}
model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime? @map("email_verified")
image String?
password String? // For credentials provider
role String @default("user")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
accounts Account[]
sessions Session[]
@@map("users")
}
model VerificationToken {
identifier String
token String @unique
expires DateTime
@@unique([identifier, token])
@@map("verificationtokens")
}3. Generate Prisma Client
npx prisma generate
npx prisma db push4. Prisma Client Singleton
// lib/prisma.ts
import { PrismaClient } from "@prisma/client";
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
export const prisma = globalForPrisma.prisma ?? new PrismaClient();
if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prisma;5. Auth Configuration with Prisma Adapter
// auth.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: {
strategy: "database", // Use database sessions
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // 24 hours
},
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await prisma.user.findUnique({
where: { email: credentials.email as string },
});
if (!user || !user.password) {
return null;
}
const isValid = await bcrypt.compare(
credentials.password as string,
user.password
);
if (!isValid) {
return null;
}
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
role: user.role,
};
},
}),
],
callbacks: {
async session({ session, user }) {
// With database sessions, user is the database user
if (session.user) {
session.user.id = user.id;
session.user.role = (user as any).role;
}
return session;
},
},
});Drizzle ORM Setup
1. Install Dependencies
npm install @auth/drizzle-adapter drizzle-orm
npm install -D drizzle-kit2. Database Schema
// lib/db/schema.ts
import {
pgTable,
text,
timestamp,
primaryKey,
integer,
uuid,
} from "drizzle-orm/pg-core";
import type { AdapterAccount } from "next-auth/adapters";
export const users = pgTable("user", {
id: text("id")
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: text("name"),
email: text("email").notNull().unique(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
image: text("image"),
password: text("password"),
role: text("role").default("user"),
createdAt: timestamp("createdAt", { mode: "date" }).defaultNow(),
updatedAt: timestamp("updatedAt", { mode: "date" }).defaultNow(),
});
export const accounts = pgTable(
"account",
{
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").$type<AdapterAccount["type"]>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("providerAccountId").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
},
(account) => ({
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId],
}),
})
);
export const sessions = pgTable("session", {
sessionToken: text("sessionToken").notNull().primaryKey(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
});
export const verificationTokens = pgTable(
"verificationToken",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(vt) => ({
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
})
);3. Drizzle Client
// lib/db/index.ts
import { drizzle } from "drizzle-orm/node-postgres";
import { Pool } from "pg";
import * as schema from "./schema";
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export const db = drizzle(pool, { schema });4. Auth Configuration with Drizzle Adapter
// auth.ts
import NextAuth from "next-auth";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "@/lib/db";
import * as schema from "@/lib/db/schema";
import GitHub from "next-auth/providers/github";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: DrizzleAdapter(db, {
usersTable: schema.users,
accountsTable: schema.accounts,
sessionsTable: schema.sessions,
verificationTokensTable: schema.verificationTokens,
}),
session: {
strategy: "database",
},
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
}),
],
});Custom Adapter
For custom database or cache implementations:
// lib/auth/custom-adapter.ts
import type {
Adapter,
AdapterUser,
AdapterAccount,
AdapterSession,
VerificationToken,
} from "next-auth/adapters";
export function CustomAdapter(db: any): Adapter {
return {
async createUser(user) {
const created = await db.user.create({
data: {
email: user.email,
name: user.name,
image: user.image,
emailVerified: user.emailVerified,
},
});
return created;
},
async getUser(id) {
return await db.user.findUnique({ where: { id } });
},
async getUserByEmail(email) {
return await db.user.findUnique({ where: { email } });
},
async getUserByAccount({ providerAccountId, provider }) {
const account = await db.account.findFirst({
where: { providerAccountId, provider },
include: { user: true },
});
return account?.user ?? null;
},
async updateUser(user) {
return await db.user.update({
where: { id: user.id },
data: user,
});
},
async linkAccount(account) {
await db.account.create({
data: {
userId: account.userId,
type: account.type,
provider: account.provider,
providerAccountId: account.providerAccountId,
refresh_token: account.refresh_token,
access_token: account.access_token,
expires_at: account.expires_at,
token_type: account.token_type,
scope: account.scope,
id_token: account.id_token,
session_state: account.session_state,
},
});
return account;
},
async createSession(session) {
return await db.session.create({
data: {
sessionToken: session.sessionToken,
userId: session.userId,
expires: session.expires,
},
});
},
async getSessionAndUser(sessionToken) {
const session = await db.session.findUnique({
where: { sessionToken },
include: { user: true },
});
if (!session) return null;
return {
session: {
sessionToken: session.sessionToken,
userId: session.userId,
expires: session.expires,
},
user: session.user,
};
},
async updateSession(session) {
return await db.session.update({
where: { sessionToken: session.sessionToken },
data: session,
});
},
async deleteSession(sessionToken) {
await db.session.delete({ where: { sessionToken } });
},
async createVerificationToken(token) {
await db.verificationToken.create({
data: {
identifier: token.identifier,
token: token.token,
expires: token.expires,
},
});
return token;
},
async useVerificationToken({ identifier, token }) {
try {
const deleted = await db.verificationToken.delete({
where: {
identifier_token: { identifier, token },
},
});
return deleted;
} catch {
return null;
}
},
};
}User Registration with Database
// app/actions/register.ts
"use server";
import { prisma } from "@/lib/prisma";
import { hashPassword } from "@/lib/auth";
import { redirect } from "next/navigation";
export async function registerUser(formData: FormData) {
const email = formData.get("email") as string;
const password = formData.get("password") as string;
const name = formData.get("name") as string;
// Check if user exists
const existingUser = await prisma.user.findUnique({
where: { email },
});
if (existingUser) {
return { error: "User already exists" };
}
// Hash password
const hashedPassword = await hashPassword(password);
// Create user
const user = await prisma.user.create({
data: {
email,
password: hashedPassword,
name,
},
});
redirect("/login");
}Session Management
List User Sessions
// app/actions/sessions.ts
"use server";
import { prisma } from "@/lib/prisma";
import { auth } from "@/auth";
export async function getUserSessions() {
const session = await auth();
if (!session?.user?.id) {
throw new Error("Unauthorized");
}
const sessions = await prisma.session.findMany({
where: { userId: session.user.id },
orderBy: { expires: "desc" },
});
return sessions;
}
export async function revokeSession(sessionToken: string) {
const session = await auth();
if (!session?.user?.id) {
throw new Error("Unauthorized");
}
// Verify the session belongs to the user
const targetSession = await prisma.session.findUnique({
where: { sessionToken },
});
if (targetSession?.userId !== session.user.id) {
throw new Error("Unauthorized");
}
await prisma.session.delete({
where: { sessionToken },
});
return { success: true };
}Environment Variables
# Database
DATABASE_URL="postgresql://user:password@localhost:5432/mydb"
# or
DATABASE_URL="mysql://user:password@localhost:3306/mydb"
# or
DATABASE_URL="file:./dev.db" # SQLite
# NextAuth
AUTH_SECRET="your-secret-key"
AUTH_URL="http://localhost:3000"
# OAuth Providers
GITHUB_ID="your-github-id"
GITHUB_SECRET="your-github-secret"Examples
This document contains comprehensive code examples for Next.js authentication patterns.
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.
Example 4: OAuth Sign-In Button
Input: User should be able to sign in with GitHub
Implementation:
// components/auth/sign-in-button.tsx
"use client";
import { signIn, signOut, useSession } from "next-auth/react";
export function AuthButton() {
const { data: session, status } = useSession();
if (status === "loading") {
return <button disabled>Loading...</button>;
}
if (session) {
return (
<button onClick={() => signOut()}>
Sign out {session.user?.name}
</button>
);
}
return (
<button onClick={() => signIn("github")}>
Sign in with GitHub
</button>
);
}Output: Button shows "Sign in with GitHub" for unauthenticated users, "Sign out {name}" for authenticated users.
Example 5: Credentials Provider Login
Input: Implement email/password login
Implementation:
// auth.ts
import Credentials from "next-auth/providers/credentials";
import bcrypt from "bcryptjs";
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
if (!credentials?.email || !credentials?.password) {
return null;
}
const user = await db.user.findUnique({
where: { email: credentials.email },
});
if (!user || !user.password) {
return null;
}
const isValid = await bcrypt.compare(
credentials.password,
user.password
);
return isValid
? { id: user.id, email: user.email, name: user.name }
: null;
},
}),
],
});Output: Users can authenticate with email/password against your database.
OAuth Provider Configurations
Configuration examples for popular OAuth providers with Auth.js 5.
GitHub
Setup
1. Go to GitHub Settings → Developer Settings → OAuth Apps → New OAuth App 2. Set Homepage URL: http://localhost:3000 (development) 3. Set Authorization callback URL: http://localhost:3000/api/auth/callback/github
Configuration
// auth.ts
import GitHub from "next-auth/providers/github";
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
// Allow linking accounts with same email
allowDangerousEmailAccountLinking: true,
}),
]Environment Variables
GITHUB_ID="your-github-client-id"
GITHUB_SECRET="your-github-client-secret"Custom Profile Data
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
profile(profile) {
return {
id: profile.id.toString(),
name: profile.name || profile.login,
email: profile.email,
image: profile.avatar_url,
// Custom fields
username: profile.login,
githubUrl: profile.html_url,
};
},
})Setup
1. Go to Google Cloud Console → APIs & Services → Credentials 2. Create OAuth 2.0 Client ID 3. Add authorized redirect URI: http://localhost:3000/api/auth/callback/google
Configuration
// auth.ts
import Google from "next-auth/providers/google";
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
allowDangerousEmailAccountLinking: true,
// Request additional scopes
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
scope: [
"openid",
"https://www.googleapis.com/auth/userinfo.profile",
"https://www.googleapis.com/auth/userinfo.email",
"https://www.googleapis.com/auth/calendar.readonly",
].join(" "),
},
},
}),
]Environment Variables
GOOGLE_CLIENT_ID="your-google-client-id.apps.googleusercontent.com"
GOOGLE_CLIENT_SECRET="your-google-client-secret"Access Google APIs
// In callbacks
async jwt({ token, account }) {
if (account) {
token.accessToken = account.access_token;
token.refreshToken = account.refresh_token;
}
// Refresh token if expired
if (token.expiresAt && Date.now() > (token.expiresAt as number) * 1000) {
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID!,
client_secret: process.env.GOOGLE_CLIENT_SECRET!,
grant_type: "refresh_token",
refresh_token: token.refreshToken as string,
}),
});
const tokens = await response.json();
token.accessToken = tokens.access_token;
token.expiresAt = Math.floor(Date.now() / 1000 + tokens.expires_in);
}
return token;
},Discord
Configuration
import Discord from "next-auth/providers/discord";
providers: [
Discord({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
authorization: {
params: {
scope: "identify email guilds",
},
},
profile(profile) {
return {
id: profile.id,
name: profile.global_name || profile.username,
email: profile.email,
image: profile.avatar
? `https://cdn.discordapp.com/avatars/${profile.id}/${profile.avatar}.png`
: null,
discordId: profile.id,
discriminator: profile.discriminator,
};
},
}),
]Auth0
Configuration
import Auth0 from "next-auth/providers/auth0";
providers: [
Auth0({
clientId: process.env.AUTH0_CLIENT_ID!,
clientSecret: process.env.AUTH0_CLIENT_SECRET!,
issuer: process.env.AUTH0_ISSUER!,
authorization: {
params: {
audience: process.env.AUTH0_AUDIENCE,
},
},
}),
]Environment Variables
AUTH0_CLIENT_ID="your-auth0-client-id"
AUTH0_CLIENT_SECRET="your-auth0-client-secret"
AUTH0_ISSUER="https://your-domain.auth0.com"
AUTH0_AUDIENCE="https://your-api-identifier"Microsoft Entra ID (Azure AD)
Configuration
import MicrosoftEntraID from "next-auth/providers/microsoft-entra-id";
providers: [
MicrosoftEntraID({
clientId: process.env.AZURE_AD_CLIENT_ID!,
clientSecret: process.env.AZURE_AD_CLIENT_SECRET!,
tenantId: process.env.AZURE_AD_TENANT_ID!,
authorization: {
params: {
scope: "openid profile email User.Read",
},
},
}),
]Environment Variables
AZURE_AD_CLIENT_ID="your-azure-client-id"
AZURE_AD_CLIENT_SECRET="your-azure-client-secret"
AZURE_AD_TENANT_ID="your-azure-tenant-id"Apple
Configuration
import Apple from "next-auth/providers/apple";
providers: [
Apple({
clientId: process.env.APPLE_ID!,
clientSecret: {
appleId: process.env.APPLE_ID!,
teamId: process.env.APPLE_TEAM_ID!,
privateKey: process.env.APPLE_PRIVATE_KEY!,
keyId: process.env.APPLE_KEY_ID!,
},
checks: ["pkce", "state"],
profile(profile) {
return {
id: profile.sub,
name: profile.name
? `${profile.name.firstName} ${profile.name.lastName}`
: null,
email: profile.email,
image: null,
};
},
}),
]Environment Variables
APPLE_ID="your-service-id"
APPLE_TEAM_ID="your-team-id"
APPLE_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
...
-----END PRIVATE KEY-----"
APPLE_KEY_ID="your-key-id"Configuration
import LinkedIn from "next-auth/providers/linkedin";
providers: [
LinkedIn({
clientId: process.env.LINKEDIN_CLIENT_ID!,
clientSecret: process.env.LINKEDIN_CLIENT_SECRET!,
authorization: {
params: {
scope: "openid profile email",
},
},
profile(profile) {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
};
},
}),
]Twitter (X)
Configuration
import Twitter from "next-auth/providers/twitter";
providers: [
Twitter({
clientId: process.env.TWITTER_CLIENT_ID!,
clientSecret: process.env.TWITTER_CLIENT_SECRET!,
version: "2.0", // OAuth 2.0
profile(profile) {
return {
id: profile.data.id,
name: profile.data.name,
email: profile.data.email,
image: profile.data.profile_image_url?.replace("_normal", ""),
username: profile.data.username,
};
},
}),
]Multiple Providers with Account Linking
// auth.ts
import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import GitHub from "next-auth/providers/github";
import Google from "next-auth/providers/google";
import Discord from "next-auth/providers/discord";
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
providers: [
GitHub({
clientId: process.env.GITHUB_ID!,
clientSecret: process.env.GITHUB_SECRET!,
allowDangerousEmailAccountLinking: true,
}),
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
allowDangerousEmailAccountLinking: true,
}),
Discord({
clientId: process.env.DISCORD_CLIENT_ID!,
clientSecret: process.env.DISCORD_CLIENT_SECRET!,
allowDangerousEmailAccountLinking: true,
}),
],
callbacks: {
async signIn({ user, account, profile, email, credentials }) {
// Allow all OAuth sign-ins
if (account?.provider !== "credentials") {
return true;
}
// Additional validation for credentials
return true;
},
},
});Provider-Specific UI Components
Social Login Buttons
// components/auth/social-login.tsx
"use client";
import { signIn } from "next-auth/react";
import { Github, Chrome } from "lucide-react";
export function SocialLogin() {
return (
<div className="grid gap-2">
<button
onClick={() => signIn("github", { callbackUrl: "/dashboard" })}
className="flex items-center justify-center gap-2 rounded-lg border p-2 hover:bg-gray-50"
>
<Github className="h-5 w-5" />
Continue with GitHub
</button>
<button
onClick={() => signIn("google", { callbackUrl: "/dashboard" })}
className="flex items-center justify-center gap-2 rounded-lg border p-2 hover:bg-gray-50"
>
<Chrome className="h-5 w-5" />
Continue with Google
</button>
</div>
);
}Login Page with OAuth
// app/login/page.tsx
import { SocialLogin } from "@/components/auth/social-login";
import { CredentialsForm } from "@/components/auth/credentials-form";
export default function LoginPage() {
return (
<div className="mx-auto max-w-sm space-y-6">
<div className="space-y-2 text-center">
<h1 className="text-2xl font-bold">Sign In</h1>
<p className="text-gray-500">Choose your sign-in method</p>
</div>
<SocialLogin />
<div className="relative">
<div className="absolute inset-0 flex items-center">
<span className="w-full border-t" />
</div>
<div className="relative flex justify-center text-xs uppercase">
<span className="bg-background px-2 text-muted-foreground">
Or continue with
</span>
</div>
</div>
<CredentialsForm />
</div>
);
}Testing Authentication Patterns
Testing strategies for Next.js authentication with Vitest and Playwright.
Unit Testing with Vitest
Setup
npm install -D vitest @vitejs/plugin-react jsdom @testing-library/react @testing-library/jest-dom// vitest.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./tests/setup.ts"],
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./"),
},
},
});// tests/setup.ts
import "@testing-library/jest-dom";
import { vi } from "vitest";
// Mock next/navigation
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
useRouter: () => ({
push: vi.fn(),
replace: vi.fn(),
refresh: vi.fn(),
}),
usePathname: () => "/",
useSearchParams: () => new URLSearchParams(),
}));
// Mock next/headers
vi.mock("next/headers", () => ({
cookies: () => ({
get: vi.fn(),
set: vi.fn(),
}),
headers: () => new Headers(),
}));Testing Auth Module
// tests/auth/auth.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { auth } from "@/auth";
vi.mock("@/auth", () => ({
auth: vi.fn(),
signIn: vi.fn(),
signOut: vi.fn(),
}));
describe("Authentication", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("Session Verification", () => {
it("should return null when user is not authenticated", async () => {
vi.mocked(auth).mockResolvedValue(null);
const session = await auth();
expect(session).toBeNull();
});
it("should return session when user is authenticated", async () => {
const mockSession = {
user: {
id: "user-1",
email: "test@example.com",
name: "Test User",
role: "user",
},
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
};
vi.mocked(auth).mockResolvedValue(mockSession);
const session = await auth();
expect(session).toEqual(mockSession);
expect(session?.user?.id).toBe("user-1");
});
});
describe("Role-Based Access", () => {
it("should identify admin users", async () => {
const adminSession = {
user: {
id: "admin-1",
email: "admin@example.com",
role: "admin",
},
};
vi.mocked(auth).mockResolvedValue(adminSession);
const session = await auth();
expect(session?.user?.role).toBe("admin");
});
});
});Testing DAL Functions
// tests/auth/dal.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { verifySession, requireAuth, requireRole } from "@/lib/dal";
import { auth } from "@/auth";
import { redirect } from "next/navigation";
vi.mock("@/auth", () => ({
auth: vi.fn(),
}));
vi.mock("next/navigation", () => ({
redirect: vi.fn(),
}));
describe("Data Access Layer", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("verifySession", () => {
it("should return session for authenticated user", async () => {
const mockSession = {
user: { id: "1", email: "test@example.com", role: "user" },
};
vi.mocked(auth).mockResolvedValue(mockSession);
const result = await verifySession();
expect(result).toEqual(mockSession);
});
it("should return null for unauthenticated user", async () => {
vi.mocked(auth).mockResolvedValue(null);
const result = await verifySession();
expect(result).toBeNull();
});
});
describe("requireAuth", () => {
it("should return session when authenticated", async () => {
const mockSession = {
user: { id: "1", email: "test@example.com" },
};
vi.mocked(auth).mockResolvedValue(mockSession);
const result = await requireAuth();
expect(result).toEqual(mockSession);
});
it("should redirect to login when not authenticated", async () => {
vi.mocked(auth).mockResolvedValue(null);
await requireAuth();
expect(redirect).toHaveBeenCalledWith("/login");
});
});
describe("requireRole", () => {
it("should return session for correct role", async () => {
const mockSession = {
user: { id: "1", email: "admin@example.com", role: "admin" },
};
vi.mocked(auth).mockResolvedValue(mockSession);
const result = await requireRole("admin");
expect(result).toEqual(mockSession);
});
it("should redirect to unauthorized for wrong role", async () => {
const mockSession = {
user: { id: "1", email: "user@example.com", role: "user" },
};
vi.mocked(auth).mockResolvedValue(mockSession);
await requireRole("admin");
expect(redirect).toHaveBeenCalledWith("/unauthorized");
});
});
});Testing Server Actions
// tests/actions/todo.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { createTodo } from "@/app/actions/todo";
import { auth } from "@/auth";
vi.mock("@/auth", () => ({
auth: vi.fn(),
}));
vi.mock("next/cache", () => ({
revalidatePath: vi.fn(),
}));
vi.mock("@/lib/db", () => ({
db: {
todo: {
create: vi.fn(),
},
},
}));
describe("Todo Actions", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("createTodo", () => {
it("should throw error when user is not authenticated", async () => {
vi.mocked(auth).mockResolvedValue(null);
const formData = new FormData();
formData.append("title", "Test Todo");
await expect(createTodo(formData)).rejects.toThrow("Unauthorized");
});
it("should create todo for authenticated user", async () => {
const mockSession = {
user: { id: "user-1", email: "test@example.com", role: "user" },
};
vi.mocked(auth).mockResolvedValue(mockSession);
const formData = new FormData();
formData.append("title", "Test Todo");
const { db } = await import("@/lib/db");
vi.mocked(db.todo.create).mockResolvedValue({
id: "1",
title: "Test Todo",
userId: "user-1",
});
await createTodo(formData);
expect(db.todo.create).toHaveBeenCalledWith({
data: {
title: "Test Todo",
userId: "user-1",
},
});
});
});
});Testing React Components
// tests/components/sign-in-button.test.tsx
import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, fireEvent } from "@testing-library/react";
import { SignInButton } from "@/components/auth/sign-in-button";
import { useSession, signIn, signOut } from "next-auth/react";
vi.mock("next-auth/react", () => ({
useSession: vi.fn(),
signIn: vi.fn(),
signOut: vi.fn(),
}));
describe("SignInButton", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should show loading state", () => {
vi.mocked(useSession).mockReturnValue({
data: null,
status: "loading",
update: vi.fn(),
});
render(<SignInButton />);
expect(screen.getByText("Loading...")).toBeDisabled();
});
it("should show sign in button when unauthenticated", () => {
vi.mocked(useSession).mockReturnValue({
data: null,
status: "unauthenticated",
update: vi.fn(),
});
render(<SignInButton />);
const button = screen.getByText("Sign in with GitHub");
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(signIn).toHaveBeenCalledWith("github");
});
it("should show sign out button when authenticated", () => {
vi.mocked(useSession).mockReturnValue({
data: {
user: { name: "John Doe", email: "john@example.com" },
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
},
status: "authenticated",
update: vi.fn(),
});
render(<SignInButton />);
const button = screen.getByText("Sign out John Doe");
expect(button).toBeInTheDocument();
fireEvent.click(button);
expect(signOut).toHaveBeenCalled();
});
});Integration Testing
Testing API Routes
// tests/api/auth.test.ts
import { describe, it, expect, vi, beforeEach } from "vitest";
import { GET } from "@/app/api/user/route";
import { auth } from "@/auth";
vi.mock("@/auth", () => ({
auth: vi.fn(),
}));
vi.mock("@/lib/db", () => ({
db: {
user: {
findUnique: vi.fn(),
},
},
}));
describe("API Routes", () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe("GET /api/user", () => {
it("should return 401 when not authenticated", async () => {
vi.mocked(auth).mockResolvedValue(null);
const response = await GET();
expect(response.status).toBe(401);
expect(await response.json()).toEqual({ error: "Unauthorized" });
});
it("should return user data when authenticated", async () => {
const mockSession = {
user: { id: "user-1", email: "test@example.com" },
};
vi.mocked(auth).mockResolvedValue(mockSession);
const mockUser = {
id: "user-1",
name: "Test User",
email: "test@example.com",
};
const { db } = await import("@/lib/db");
vi.mocked(db.user.findUnique).mockResolvedValue(mockUser);
const response = await GET();
expect(response.status).toBe(200);
expect(await response.json()).toEqual(mockUser);
});
});
});E2E Testing with Playwright
Setup
npm install -D @playwright/test
npx playwright install// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "http://localhost:3000",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
});Auth Fixtures
// e2e/fixtures.ts
import { test as base, expect } from "@playwright/test";
export * from "@playwright/test";
export const test = base.extend<{
login: (user?: { email: string; password: string }) => Promise<void>;
}>({
login: async ({ page }, use) => {
await use(async (user) => {
await page.goto("/login");
const email = user?.email || "test@example.com";
const password = user?.password || "password123";
await page.fill('input[name="email"]', email);
await page.fill('input[name="password"]', password);
await page.click('button[type="submit"]');
await page.waitForURL("/dashboard");
});
},
});Auth Flow Tests
// e2e/auth.spec.ts
import { test, expect } from "./fixtures";
test.describe("Authentication Flows", () => {
test("should redirect to login when accessing protected page", async ({
page,
}) => {
await page.goto("/dashboard");
await expect(page).toHaveURL("/login?callbackUrl=%2Fdashboard");
});
test("should login with credentials", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
await expect(page).toHaveURL("/dashboard");
await expect(page.locator("h1")).toContainText("Dashboard");
});
test("should show error for invalid credentials", async ({ page }) => {
await page.goto("/login");
await page.fill('input[name="email"]', "wrong@example.com");
await page.fill('input[name="password"]', "wrongpassword");
await page.click('button[type="submit"]');
await expect(page.locator("text=Invalid credentials")).toBeVisible();
});
test("should logout user", async ({ page, login }) => {
await login();
await page.click("text=Sign out");
await expect(page).toHaveURL("/");
await page.goto("/dashboard");
await expect(page).toHaveURL("/login");
});
});
test.describe("Protected Routes", () => {
test("should allow admin to access admin page", async ({ page, login }) => {
// Login as admin
await login({ email: "admin@example.com", password: "admin123" });
await page.goto("/admin");
await expect(page.locator("h1")).toContainText("Admin");
});
test("should redirect non-admin from admin page", async ({ page, login }) => {
// Login as regular user
await login({ email: "user@example.com", password: "user123" });
await page.goto("/admin");
await expect(page).toHaveURL("/unauthorized");
});
});Storage State for Authenticated Tests
// e2e/global-setup.ts
import { chromium, FullConfig } from "@playwright/test";
async function globalSetup(config: FullConfig) {
const { baseURL } = config.projects[0].use;
const browser = await chromium.launch();
const page = await browser.newPage();
// Login and save storage state
await page.goto(`${baseURL}/login`);
await page.fill('input[name="email"]', "test@example.com");
await page.fill('input[name="password"]', "password123");
await page.click('button[type="submit"]');
await page.waitForURL(`${baseURL}/dashboard`);
await page.context().storageState({ path: "e2e/.auth/user.json" });
// Admin login
await page.goto(`${baseURL}/login`);
await page.fill('input[name="email"]', "admin@example.com");
await page.fill('input[name="password"]', "admin123");
await page.click('button[type="submit"]');
await page.waitForURL(`${baseURL}/dashboard`);
await page.context().storageState({ path: "e2e/.auth/admin.json" });
await browser.close();
}
export default globalSetup;// playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
globalSetup: require.resolve("./e2e/global-setup"),
projects: [
{
name: "authenticated",
use: {
...devices["Desktop Chrome"],
storageState: "e2e/.auth/user.json",
},
},
{
name: "admin",
use: {
...devices["Desktop Chrome"],
storageState: "e2e/.auth/admin.json",
},
},
],
});Mocking Auth in Stories (Storybook)
// .storybook/preview.tsx
import type { Preview } from "@storybook/react";
import { SessionProvider } from "next-auth/react";
const preview: Preview = {
decorators: [
(Story, context) => {
const session = context.parameters.session || null;
return (
<SessionProvider session={session}>
<Story />
</SessionProvider>
);
},
],
};
export default preview;// components/auth/sign-in-button.stories.tsx
import type { Meta, StoryObj } from "@storybook/react";
import { SignInButton } from "./sign-in-button";
const meta: Meta<typeof SignInButton> = {
component: SignInButton,
};
export default meta;
type Story = StoryObj<typeof SignInButton>;
export const Unauthenticated: Story = {
parameters: {
session: null,
},
};
export const Authenticated: Story = {
parameters: {
session: {
user: {
name: "John Doe",
email: "john@example.com",
image: "https://example.com/avatar.jpg",
},
expires: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
},
},
};
export const Loading: Story = {
parameters: {
session: undefined,
},
};Related skills
Forks & variants (1)
Nextjs Authentication has 1 known copy in the catalog totaling 21 installs. They canonicalize to this original listing.
- giuseppe-trisciuoglio - 21 installs
How it compares
Use nextjs-authentication for opinionated Auth.js v5 App Router scaffolding; use generic OAuth skills when auth is outside Next.js or on a different framework.
FAQ
What does nextjs-authentication do?
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
When should I use nextjs-authentication?
During build backend work for backend & apis.
Is nextjs-authentication safe to install?
Review the Security Audits panel on this listing before production use.