auth/nextauth-skill/SKILL.md
Set up NextAuth.js v5 (Auth.js) with providers, callbacks (jwt, session, signIn), adapter pattern (Prisma/Drizzle), middleware protection, session management, and custom pages.
npx skillsauth add achreftlili/deep-dev-skills nextauth-skillInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Set up NextAuth.js v5 (Auth.js) with providers, callbacks (jwt, session, signIn), adapter pattern (Prisma/Drizzle), middleware protection, session management, and custom pages.
npm install next-auth@beta
# With Prisma adapter
npm install @auth/prisma-adapter
# With Drizzle adapter
npm install @auth/drizzle-adapter
# Generate AUTH_SECRET
npx auth secret
app/
api/
auth/
[...nextauth]/
route.ts # NextAuth route handler
auth/
signin/
page.tsx # Custom sign-in page
error/
page.tsx # Custom error page
dashboard/
page.tsx # Protected page example
src/
auth.ts # NextAuth configuration (main file)
auth.config.ts # Provider and callback config (edge-compatible)
middleware.ts # Route protection middleware
auth.ts config file that exports handlers, auth, signIn, and signOut.auth.config.ts (edge-compatible, no DB adapter) and auth.ts (full config with adapter) for middleware compatibility.auth() in server components/API routes (for data-level auth).callbacks.jwt to add custom claims, callbacks.session to expose them to the client.src/auth.config.ts)import type { NextAuthConfig } from "next-auth";
import Google from "next-auth/providers/google";
import GitHub from "next-auth/providers/github";
import Credentials from "next-auth/providers/credentials";
export const authConfig: NextAuthConfig = {
pages: {
signIn: "/auth/signin",
error: "/auth/error",
},
providers: [
Google({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
GitHub({
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
}),
Credentials({
name: "credentials",
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
// Validate credentials against your database
// This runs server-side only
const { email, password } = credentials as {
email: string;
password: string;
};
// Replace with actual DB lookup + bcrypt compare
const user = await findUserByCredentials(email, password);
if (!user) return null;
return {
id: user.id,
email: user.email,
name: user.name,
role: user.role,
image: user.avatar,
};
},
}),
],
callbacks: {
authorized({ auth, request: { nextUrl } }) {
const isLoggedIn = !!auth?.user;
const isProtected = nextUrl.pathname.startsWith("/dashboard");
if (isProtected && !isLoggedIn) {
return Response.redirect(new URL("/auth/signin", nextUrl));
}
return true;
},
jwt({ token, user, trigger, session }) {
// Add custom fields to JWT on sign-in
if (user) {
token.id = user.id;
token.role = (user as any).role ?? "user";
}
// Handle session update (e.g., after profile change)
if (trigger === "update" && session) {
token.name = session.name;
}
return token;
},
session({ session, token }) {
// Expose custom fields to client session
if (token) {
session.user.id = token.id as string;
session.user.role = token.role as string;
}
return session;
},
signIn({ user, account, profile }) {
// Block sign-in for unverified emails (OAuth)
if (account?.provider === "google") {
return (profile as any)?.email_verified === true;
}
return true;
},
},
session: {
strategy: "jwt",
maxAge: 30 * 24 * 60 * 60, // 30 days
},
};
// Helper — replace with actual implementation
async function findUserByCredentials(email: string, password: string) {
// const user = await prisma.user.findUnique({ where: { email } });
// if (!user || !await bcrypt.compare(password, user.password)) return null;
// return user;
return null;
}
src/auth.ts)import NextAuth from "next-auth";
import { PrismaAdapter } from "@auth/prisma-adapter";
import { prisma } from "@/lib/prisma";
import { authConfig } from "./auth.config";
export const {
handlers,
auth,
signIn,
signOut,
} = NextAuth({
adapter: PrismaAdapter(prisma),
...authConfig,
});
app/api/auth/[...nextauth]/route.ts)import { handlers } from "@/auth";
export const { GET, POST } = handlers;
src/middleware.ts)import NextAuth from "next-auth";
import { authConfig } from "@/auth.config";
const { auth } = NextAuth(authConfig);
export default auth;
export const config = {
// Match all routes except static files and API routes that don't need auth
matcher: [
"/((?!api/public|_next/static|_next/image|favicon.ico).*)",
],
};
types/next-auth.d.ts)import "next-auth";
declare module "next-auth" {
interface User {
role?: string;
}
interface Session {
user: {
id: string;
role: string;
email: string;
name: string;
image?: string;
};
}
}
declare module "next-auth/jwt" {
interface JWT {
id: string;
role: string;
}
}
// app/dashboard/page.tsx
import { auth } from "@/auth";
import { redirect } from "next/navigation";
export default async function DashboardPage() {
const session = await auth();
if (!session?.user) {
redirect("/auth/signin");
}
return (
<div>
<h1>Dashboard</h1>
<p>Welcome, {session.user.name}</p>
<p>Role: {session.user.role}</p>
</div>
);
}
"use client";
import { useSession, signIn, signOut } from "next-auth/react";
export function AuthButton() {
const { data: session, status } = useSession();
if (status === "loading") {
return <div>Loading...</div>;
}
if (session) {
return (
<div>
<p>Signed in as {session.user.email}</p>
<button onClick={() => signOut()}>Sign Out</button>
</div>
);
}
return <button onClick={() => signIn()}>Sign In</button>;
}
// app/layout.tsx
import { SessionProvider } from "next-auth/react";
import { auth } from "@/auth";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const session = await auth();
return (
<html lang="en">
<body>
<SessionProvider session={session}>
{children}
</SessionProvider>
</body>
</html>
);
}
app/auth/signin/page.tsx)import { signIn } from "@/auth";
export default function SignInPage() {
return (
<div className="flex min-h-screen items-center justify-center">
<div className="w-full max-w-md space-y-4 rounded-lg border p-8">
<h1 className="text-2xl font-bold">Sign In</h1>
<form
action={async () => {
"use server";
await signIn("google", { redirectTo: "/dashboard" });
}}
>
<button type="submit" className="w-full rounded bg-blue-600 px-4 py-2 text-white">
Continue with Google
</button>
</form>
<form
action={async () => {
"use server";
await signIn("github", { redirectTo: "/dashboard" });
}}
>
<button type="submit" className="w-full rounded bg-gray-800 px-4 py-2 text-white">
Continue with GitHub
</button>
</form>
<div className="relative py-4">
<div className="absolute inset-0 flex items-center">
<div className="w-full border-t" />
</div>
<div className="relative flex justify-center text-sm">
<span className="bg-white px-2 text-gray-500">Or</span>
</div>
</div>
<form
action={async (formData) => {
"use server";
await signIn("credentials", {
email: formData.get("email"),
password: formData.get("password"),
redirectTo: "/dashboard",
});
}}
className="space-y-3"
>
<input
name="email"
type="email"
placeholder="Email"
required
className="w-full rounded border px-3 py-2"
/>
<input
name="password"
type="password"
placeholder="Password"
required
className="w-full rounded border px-3 py-2"
/>
<button type="submit" className="w-full rounded bg-green-600 px-4 py-2 text-white">
Sign In with Email
</button>
</form>
</div>
</div>
);
}
"use server";
import { auth } from "@/auth";
export async function getSecretData() {
const session = await auth();
if (!session?.user) throw new Error("Unauthorized");
if (session.user.role !== "admin") throw new Error("Forbidden");
return { secret: "admin-only data" };
}
.env.local)AUTH_SECRET=generate-with-npx-auth-secret
AUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=your-google-client-id
GOOGLE_CLIENT_SECRET=your-google-client-secret
GITHUB_CLIENT_ID=your-github-client-id
GITHUB_CLIENT_SECRET=your-github-client-secret
DATABASE_URL=postgresql://app:secret@localhost:5432/myapp
# Generate AUTH_SECRET
npx auth secret
# Start development
npm run dev
# Test auth flow
open http://localhost:3000/auth/signin
# Debug: view session
open http://localhost:3000/api/auth/session
prisma-schema-starter skill for the complete schema definition.drizzle-starter skill and add the required auth tables (users, accounts, sessions, verification_tokens).jwt-auth-skill for token-based auth. NextAuth is best for browser-based Next.js apps.useSession() from next-auth/react in client components and auth() in server components.auth() in tests. For E2E tests with Playwright, set auth cookies programmatically before visiting protected pages.testing
Set up Vitest 2.x with TypeScript for unit and component testing using test/describe/it, vi.fn/vi.mock/vi.spyOn, component testing with Testing Library, coverage (v8/istanbul), workspace config, and snapshot testing.
testing
Set up pytest 8.x with Python for unit and integration testing using fixtures (scope, autouse, parametrize), async tests (pytest-asyncio), mocking (unittest.mock, pytest-mock), coverage (pytest-cov), conftest.py patterns, and markers.
testing
Set up Playwright 1.49+ with TypeScript for E2E testing using page object model, fixtures, test.describe/test blocks, assertions, selectors, network mocking, CI configuration, and trace viewer.
testing
Set up Jest 30+ with TypeScript for unit tests, integration tests, mocking (jest.fn, jest.mock, jest.spyOn), coverage configuration, custom matchers, snapshot testing, and setup/teardown patterns.