npx skills add ...
npx skills add gocallum/nextjs16-agent-skills --skill authjs-skills
Auth.js v5 setup for Next.js authentication including Google OAuth, credentials provider, environment configuration, and core API integration
npx skills add gocallum/nextjs16-agent-skills --skill authjs-skills
Note: Auth.js v5 is currently in beta. Use next-auth@beta to install the latest v5 version.
auth() Export: Single function for authentication across all contextsImportant: Never commit AUTH_SECRET to version control. Use .env.local for development.
auth.ts Configuration FileCreate auth.ts at the project root (next to package.json):
Note: This is a basic setup example. For production-ready credentials authentication, see the "Credentials Provider" section below which includes proper password hashing with bcrypt and database integration.
Create app/api/auth/[...nextauth]/route.ts:
Create middleware.ts at the project root:
For more control:
http://localhost:3000/api/auth/callback/googlehttps://yourdomain.com/api/auth/callback/google.env.localAuth.js v5 supports two session strategies:
For persisting users, accounts, and sessions in a database, install the Prisma adapter:
Then configure it in your auth.ts:
Required Prisma schema:
redirect callback to prevent open redirectsupdateAge to refresh session datanext-auth package remains the same, but imports are simplifiedauth(): Replace getServerSession with auth()auth as middleware directlyAUTH_SECRET not set:
Generate and set AUTH_SECRET in .env.local
Google OAuth redirect mismatch:
Ensure redirect URI in Google Console matches: http://localhost:3000/api/auth/callback/google
Session not persisting:
AUTH_URL is set correctlysessionToken cookie is being set (check browser DevTools)TypeScript errors with session:
Session and JWT types using module augmentationpnpm tsc --noEmit to check for type errorsCredentials provider not working:
session.strategy is set to "jwt"authorize function returns correct user object with id field# Generate a random secret (Unix/Linux/macOS)
openssl rand -base64 32
# Alternative using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"
# Using pnpm
pnpm dlx auth secretimport NextAuth from "next-auth"
import Google from "next-auth/providers/google"
import Credentials from "next-auth/providers/credentials"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
}),
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
// TODO: Implement your authentication logic here
// This is a basic example - see Credentials Provider section below for complete implementation
if (!credentials?.email || !credentials?.password) {
return null
}
// Example: validate against database (placeholder)
// See "Credentials Provider" section for full implementation with bcrypt
const user = { id: "1", email: credentials.email, name: "User" } // Replace with actual DB lookup
if (!user) {
return null
}
return {
id: user.id,
email: user.email,
name: user.name,
}
},
}),
],
pages: {
signIn: '/auth/signin',
},
callbacks: {
authorized: async ({ auth }) => {
// Return true if user is authenticated
return !!auth
},
},
})import { handlers } from "@/auth"
export const { GET, POST } = handlersexport { auth as middleware } from "@/auth"
export const config = {
matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"],
}import { auth } from "@/auth"
export default auth((req) => {
const isLoggedIn = !!req.auth
const isOnDashboard = req.nextUrl.pathname.startsWith('/dashboard')
if (isOnDashboard && !isLoggedIn) {
return Response.redirect(new URL('/auth/signin', req.url))
}
})
export const config = {
matcher: ['/dashboard/:path*', '/profile/:path*'],
}import NextAuth from "next-auth"
import Google from "next-auth/providers/google"
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code"
}
}
}),
],
})Google({
clientId: process.env.AUTH_GOOGLE_ID,
clientSecret: process.env.AUTH_GOOGLE_SECRET,
// Request additional scopes
authorization: {
params: {
scope: "openid email profile",
prompt: "select_account", // Force account selection
}
},
// Allow specific domains only
allowDangerousEmailAccountLinking: false,
})# Install required packages for credentials provider
pnpm add bcryptjs zod
pnpm add -D @types/bcryptjsimport NextAuth from "next-auth"
import Credentials from "next-auth/providers/credentials"
import { z } from "zod"
import bcrypt from "bcryptjs"
import { prisma } from "@/lib/prisma"
const credentialsSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
})
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email", placeholder: "user@example.com" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials) => {
try {
const { email, password } = credentialsSchema.parse(credentials)
// Fetch user from database
const user = await prisma.user.findUnique({
where: { email },
})
if (!user) {
throw new Error("User not found")
}
// Verify password
const isValidPassword = await bcrypt.compare(password, user.hashedPassword)
if (!isValidPassword) {
throw new Error("Invalid password")
}
// Return user object (must include id)
return {
id: user.id,
email: user.email,
name: user.name,
image: user.image,
}
} catch (error) {
console.error("Authentication error:", error)
return null
}
},
}),
],
session: {
strategy: "jwt", // Required for credentials provider
},
})// app/api/auth/register/route.ts
import { NextResponse } from "next/server"
import bcrypt from "bcryptjs"
import { z } from "zod"
import { prisma } from "@/lib/prisma"
const registerSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
name: z.string().min(2),
})
export async function POST(req: Request) {
try {
const body = await req.json()
const { email, password, name } = registerSchema.parse(body)
// Check if user exists
const existingUser = await prisma.user.findUnique({
where: { email },
})
if (existingUser) {
return NextResponse.json(
{ error: "User already exists" },
{ status: 400 }
)
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10)
// Create user
const user = await prisma.user.create({
data: {
email,
name,
hashedPassword,
},
})
return NextResponse.json(
{ message: "User created successfully", userId: user.id },
{ status: 201 }
)
} catch (error) {
console.error("Registration error:", error)
return NextResponse.json(
{ error: "Failed to register user" },
{ status: 500 }
)
}
}import { auth } from "@/auth"
export default async function ProfilePage() {
const session = await auth()
if (!session?.user) {
return <div>Not authenticated</div>
}
return (
<div>
<h1>Welcome, {session.user.name}!</h1>
<p>Email: {session.user.email}</p>
</div>
)
}"use server"
import { auth } from "@/auth"
import { revalidatePath } from "next/cache"
import { prisma } from "@/lib/prisma"
export async function updateProfile(formData: FormData) {
const session = await auth()
if (!session?.user) {
throw new Error("Not authenticated")
}
const name = formData.get("name") as string
// Update database
await prisma.user.update({
where: { id: session.user.id },
data: { name },
})
revalidatePath("/profile")
}// app/providers.tsx
"use client"
import { SessionProvider } from "next-auth/react"
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>
}// app/layout.tsx
import { Providers } from "./providers"
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}// app/components/user-profile.tsx
"use client"
import { useSession, signIn, signOut } from "next-auth/react"
export function UserProfile() {
const { data: session, status } = useSession()
if (status === "loading") {
return <div>Loading...</div>
}
if (!session) {
return (
<button onClick={() => signIn()}>
Sign In
</button>
)
}
return (
<div>
<p>Signed in as {session.user?.email}</p>
<button onClick={() => signOut()}>
Sign Out
</button>
</div>
)
}import { signIn } from "@/auth"
// Server Action
export async function handleSignIn(provider: string) {
"use server"
await signIn(provider)
}
// With credentials
export async function handleCredentialsSignIn(formData: FormData) {
"use server"
await signIn("credentials", formData)
}
// With redirect
export async function handleGoogleSignIn() {
"use server"
await signIn("google", { redirectTo: "/dashboard" })
}// app/auth/signin/page.tsx
import { signIn } from "@/auth"
export default function SignInPage() {
return (
<div>
<h1>Sign In</h1>
{/* Google OAuth */}
<form
action={async () => {
"use server"
await signIn("google")
}}
>
<button type="submit">Sign in with Google</button>
</form>
{/* Credentials */}
<form
action={async (formData) => {
"use server"
await signIn("credentials", formData)
}}
>
<input name="email" type="email" placeholder="Email" required />
<input name="password" type="password" placeholder="Password" required />
<button type="submit">Sign In</button>
</form>
</div>
)
}import { signOut } from "@/auth"
export default function SignOutButton() {
return (
<form
action={async () => {
"use server"
await signOut()
}}
>
<button type="submit">Sign Out</button>
</form>
)
}export const { handlers, signIn, signOut, auth } = NextAuth({
session: {
strategy: "jwt", // or "database"
maxAge: 30 * 24 * 60 * 60, // 30 days
updateAge: 24 * 60 * 60, // 24 hours
},
})import NextAuth from "next-auth"
import type { DefaultSession } from "next-auth"
declare module "next-auth" {
interface Session {
user: {
id: string
role: string
} & DefaultSession["user"]
}
}
export const { handlers, signIn, signOut, auth } = NextAuth({
callbacks: {
jwt({ token, user }) {
if (user) {
token.id = user.id
token.role = user.role
}
return token
},
session({ session, token }) {
if (session.user) {
session.user.id = token.id as string
session.user.role = token.role as string
}
return session
},
},
})export const { handlers, signIn, signOut, auth } = NextAuth({
callbacks: {
// Called when user signs in
async signIn({ user, account, profile }) {
// Return true to allow sign in, false to deny
// Example: Check if email is verified
if (account?.provider === "google") {
return profile?.email_verified === true
}
return true
},
// Called whenever a JWT is created or updated
async jwt({ token, user, account }) {
if (user) {
token.id = user.id
}
if (account) {
token.accessToken = account.access_token
}
return token
},
// Called whenever a session is checked
async session({ session, token }) {
session.user.id = token.id as string
session.accessToken = token.accessToken as string
return session
},
// Called on middleware and server-side auth checks
async authorized({ auth, request }) {
const isLoggedIn = !!auth?.user
const isOnDashboard = request.nextUrl.pathname.startsWith("/dashboard")
if (isOnDashboard) {
return isLoggedIn
}
return true
},
// Called when user is redirected
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
},
},
})pnpm add @auth/prisma-adapterimport NextAuth from "next-auth"
import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"
export const { handlers, signIn, signOut, auth } = NextAuth({
adapter: PrismaAdapter(prisma),
session: {
strategy: "database",
},
providers: [
// ... providers
],
})model User {
id String @id @default(cuid())
name String?
email String @unique
emailVerified DateTime?
image String?
accounts Account[]
sessions Session[]
}
model Account {
id String @id @default(cuid())
userId String
type String
provider String
providerAccountId String
refresh_token String?
access_token String?
expires_at Int?
token_type String?
scope String?
id_token String?
session_state String?
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([provider, providerAccountId])
}
model Session {
id String @id @default(cuid())
sessionToken String @unique
userId String
expires DateTime
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
}// app/api/user/route.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
export async function GET() {
const session = await auth()
if (!session?.user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
)
}
return NextResponse.json({
user: session.user,
})
}// lib/auth-helpers.ts
import { auth } from "@/auth"
import { NextResponse } from "next/server"
import type { Session } from "next-auth"
export async function withAuth(
handler: (session: Session) => Promise<NextResponse>
) {
const session = await auth()
if (!session?.user) {
return NextResponse.json(
{ error: "Unauthorized" },
{ status: 401 }
)
}
return handler(session)
}
// Usage
export async function GET() {
return withAuth(async (session) => {
return NextResponse.json({ userId: session.user.id })
})
}