Set up NextAuth.js v5 (Auth.js) with providers, callbacks (jwt, session, signIn), adapter pattern (Prisma/Drizzle), middleware protection, session management, and custom pages.
87
85%
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Passed
No known issues
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 secretapp/
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 middlewareauth.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/sessionprisma-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.181fcbc
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.