--- name: authjs-skills description: Auth.js v5 setup for Next.js authentication including Google OAuth, credentials provider, environment configuration, and core API integration --- ## Links - Getting Started: https://authjs.dev/getting-started/installation?framework=Next.js - Migrating to v5: https://authjs.dev/getting-started/migrating-to-v5 - Google Provider: https://authjs.dev/getting-started/providers/google - Credentials Provider: https://authjs.dev/getting-started/providers/credentials - Core API Reference: https://authjs.dev/reference/core - Session Management: https://authjs.dev/getting-started/session-management - Concepts: https://authjs.dev/concepts ## Installation ```sh pnpm add next-auth@beta ``` **Note**: Auth.js v5 is currently in beta. Use `next-auth@beta` to install the latest v5 version. ## What's New in Auth.js v5? ### Key Changes from v4 - **Simplified Configuration**: More streamlined setup with better TypeScript support - **Universal `auth()` Export**: Single function for authentication across all contexts - **Enhanced Security**: Improved CSRF protection and session handling - **Edge Runtime Support**: Full compatibility with Edge Runtime and middleware - **Better Type Safety**: Improved TypeScript definitions throughout ## Environment Variables ### Required Environment Variables ```env # Auth.js Configuration AUTH_SECRET=your_secret_key_here # Google OAuth (if using Google provider) AUTH_GOOGLE_ID=your_google_client_id AUTH_GOOGLE_SECRET=your_google_client_secret # For production deployments AUTH_URL=https://yourdomain.com # For development (optional, defaults to http://localhost:3000) # AUTH_URL=http://localhost:3000 ``` ### Generating AUTH_SECRET ```sh # 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 secret ``` **Important**: Never commit `AUTH_SECRET` to version control. Use `.env.local` for development. ## Basic Setup (Next.js App Router) ### 1. Create `auth.ts` Configuration File Create `auth.ts` at the project root (next to `package.json`): ```typescript import 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 }, }, }) ``` **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. ### 2. Create API Route Handler Create `app/api/auth/[...nextauth]/route.ts`: ```typescript import { handlers } from "@/auth" export const { GET, POST } = handlers ``` ### 3. Add Middleware (Optional but Recommended) Create `middleware.ts` at the project root: ```typescript export { auth as middleware } from "@/auth" export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], } ``` For more control: ```typescript 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*'], } ``` ## Google OAuth Provider ### 1. Google Cloud Console Setup 1. Go to [Google Cloud Console](https://console.cloud.google.com/) 2. Create a new project or select existing 3. Enable Google+ API 4. Create OAuth 2.0 credentials: - Application type: Web application - Authorized redirect URIs: - Development: `http://localhost:3000/api/auth/callback/google` - Production: `https://yourdomain.com/api/auth/callback/google` 5. Copy Client ID and Client Secret to `.env.local` ### 2. Configuration ```typescript 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" } } }), ], }) ``` ### 3. Google Provider Options ```typescript 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, }) ``` ## Credentials Provider (Username/Password) ### Required Dependencies ```sh # Install required packages for credentials provider pnpm add bcryptjs zod pnpm add -D @types/bcryptjs ``` ### 1. Basic Configuration ```typescript import 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 }, }) ``` ### 2. User Registration Example ```typescript // 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 } ) } } ``` ## Using Auth in Components ### Server Components ```typescript import { auth } from "@/auth" export default async function ProfilePage() { const session = await auth() if (!session?.user) { return
Not authenticated
} return (

Welcome, {session.user.name}!

Email: {session.user.email}

) } ``` ### Server Actions ```typescript "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") } ``` ### Client Components (with SessionProvider) ```typescript // app/providers.tsx "use client" import { SessionProvider } from "next-auth/react" export function Providers({ children }: { children: React.ReactNode }) { return {children} } ``` ```typescript // app/layout.tsx import { Providers } from "./providers" export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) } ``` ```typescript // 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
Loading...
} if (!session) { return ( ) } return (

Signed in as {session.user?.email}

) } ``` ## Sign In/Out Actions ### Programmatic Sign In ```typescript 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" }) } ``` ### Sign In Form Component ```typescript // app/auth/signin/page.tsx import { signIn } from "@/auth" export default function SignInPage() { return (

Sign In

{/* Google OAuth */}
{ "use server" await signIn("google") }} >
{/* Credentials */}
{ "use server" await signIn("credentials", formData) }} >
) } ``` ### Sign Out ```typescript import { signOut } from "@/auth" export default function SignOutButton() { return (
{ "use server" await signOut() }} >
) } ``` ## Session Management ### Session Strategy Auth.js v5 supports two session strategies: 1. **JWT (Default)**: Stores session in encrypted JWT token 2. **Database**: Stores session in database ```typescript 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 }, }) ``` ### Extending the Session ```typescript 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 }, }, }) ``` ## Callbacks ### Essential Callbacks ```typescript 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 }, }, }) ``` ## Database Adapter (Optional) For persisting users, accounts, and sessions in a database, install the Prisma adapter: ```sh pnpm add @auth/prisma-adapter ``` Then configure it in your `auth.ts`: ```typescript import 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 ], }) ``` Required Prisma schema: ```prisma 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) } ``` ## API Routes ### Custom API Endpoints ```typescript // 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, }) } ``` ### Protected Route Helper ```typescript // 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 ) { 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 }) }) } ``` ## Best Practices ### Security - **Always hash passwords**: Use bcrypt, argon2, or similar - **Use HTTPS in production**: Required for secure cookie transmission - **Validate environment variables**: Check AUTH_SECRET and provider credentials - **Set secure cookie options**: ```typescript cookies: { sessionToken: { name: `__Secure-next-auth.session-token`, options: { httpOnly: true, sameSite: 'lax', path: '/', secure: process.env.NODE_ENV === 'production', }, }, } ``` - **Implement rate limiting**: Protect sign-in endpoints - **Use CSRF protection**: Enabled by default in v5 - **Validate redirects**: Use the `redirect` callback to prevent open redirects ### Session Management - **Use appropriate maxAge**: Default 30 days, adjust based on security requirements - **Update sessions regularly**: Use `updateAge` to refresh session data - **Handle session expiry gracefully**: Provide clear UI feedback - **Secure session storage**: Use database strategy for sensitive applications ### Provider Configuration - **Google OAuth**: Request minimum required scopes - **Credentials**: Always validate input with zod or similar - **Multiple providers**: Allow account linking carefully - **Provider-specific logic**: Use callbacks to handle provider differences ### Performance - **Cache session checks**: Use middleware for route protection - **Minimize database calls**: Use JWT strategy when appropriate - **Optimize database queries**: Add indexes on frequently queried fields - **Use Edge Runtime**: For faster authentication checks in middleware ### Type Safety - **Extend types properly**: Use module augmentation for custom session fields - **Validate inputs**: Use zod for runtime type checking - **TypeScript strict mode**: Enable for better type safety ## Common Patterns ### Protected Pages with Middleware ```typescript import { auth } from "@/auth" import { NextResponse } from "next/server" export default auth((req) => { const isLoggedIn = !!req.auth const { pathname } = req.nextUrl // Public routes const publicRoutes = ['/auth/signin', '/auth/register', '/'] if (publicRoutes.includes(pathname)) { return NextResponse.next() } // Protected routes if (!isLoggedIn) { const signInUrl = new URL('/auth/signin', req.url) signInUrl.searchParams.set('callbackUrl', pathname) return NextResponse.redirect(signInUrl) } // Role-based access const adminRoutes = ['/admin'] if (adminRoutes.some(route => pathname.startsWith(route))) { if (req.auth.user.role !== 'admin') { return NextResponse.redirect(new URL('/unauthorized', req.url)) } } return NextResponse.next() }) export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], } ``` ### Multi-Provider Setup ```typescript import NextAuth from "next-auth" import Google from "next-auth/providers/google" import GitHub from "next-auth/providers/github" import Credentials from "next-auth/providers/credentials" import { prisma } from "@/lib/prisma" export const { handlers, signIn, signOut, auth } = NextAuth({ providers: [ Google({ clientId: process.env.AUTH_GOOGLE_ID, clientSecret: process.env.AUTH_GOOGLE_SECRET, }), GitHub({ clientId: process.env.AUTH_GITHUB_ID, clientSecret: process.env.AUTH_GITHUB_SECRET, }), Credentials({ // ... credentials config }), ], callbacks: { async signIn({ user, account, profile }) { // Link accounts with same email if (account?.provider !== "credentials") { const existingUser = await prisma.user.findUnique({ where: { email: user.email }, }) if (existingUser) { // Link account to existing user await prisma.account.create({ data: { userId: existingUser.id, type: account.type, provider: account.provider, providerAccountId: account.providerAccountId, access_token: account.access_token, refresh_token: account.refresh_token, }, }) } } return true }, }, }) ``` ### Custom Sign In Page ```typescript // app/auth/signin/page.tsx import { signIn } from "@/auth" import { redirect } from "next/navigation" export default function SignInPage({ searchParams, }: { searchParams: { callbackUrl?: string } }) { const callbackUrl = searchParams.callbackUrl || "/dashboard" return (

Sign In

{/* OAuth Providers */}
{ "use server" await signIn("google", { redirectTo: callbackUrl }) }} >
Or
{/* Credentials Form */}
{ "use server" try { await signIn("credentials", { email: formData.get("email"), password: formData.get("password"), redirectTo: callbackUrl, }) } catch (error) { redirect(`/auth/signin?error=CredentialsSignin&callbackUrl=${callbackUrl}`) } }} className="space-y-4" >
) } ``` ### Role-Based Access Control (RBAC) ```typescript // lib/auth-rbac.ts import { auth } from "@/auth" export type Role = "admin" | "user" | "guest" export async function checkRole(allowedRoles: Role[]) { const session = await auth() if (!session?.user) { return false } const userRole = session.user.role as Role return allowedRoles.includes(userRole) } // Usage in Server Component export default async function AdminPage() { const hasAccess = await checkRole(["admin"]) if (!hasAccess) { redirect("/unauthorized") } return
Admin Dashboard
} // Usage in Server Action export async function deleteUser(userId: string) { "use server" const hasAccess = await checkRole(["admin"]) if (!hasAccess) { throw new Error("Unauthorized") } const { prisma } = await import("@/lib/prisma") await prisma.user.delete({ where: { id: userId } }) } ``` ## Migration from v4 to v5 ### Key Differences 1. **Import changes**: `next-auth` package remains the same, but imports are simplified 2. **Universal `auth()`**: Replace `getServerSession` with `auth()` 3. **Middleware**: Use `auth` as middleware directly 4. **Configuration**: More streamlined, fewer options needed ### Migration Steps ```typescript // v4 (old) import { getServerSession } from "next-auth/next" import { authOptions } from "@/lib/auth" export async function GET() { const session = await getServerSession(authOptions) } // v5 (new) import { auth } from "@/auth" export async function GET() { const session = await auth() } ``` ```typescript // v4 middleware (old) import { withAuth } from "next-auth/middleware" export default withAuth({ callbacks: { authorized: ({ token }) => !!token, }, }) // v5 middleware (new) export { auth as middleware } from "@/auth" ``` ## Troubleshooting ### Common Issues **AUTH_SECRET not set**: ``` Error: AUTH_SECRET environment variable is not set ``` Generate and set `AUTH_SECRET` in `.env.local` **Google OAuth redirect mismatch**: ``` Error: redirect_uri_mismatch ``` Ensure redirect URI in Google Console matches: `http://localhost:3000/api/auth/callback/google` **Session not persisting**: - Check `AUTH_URL` is set correctly - Verify cookies are not blocked - Ensure `sessionToken` cookie is being set (check browser DevTools) **TypeScript errors with session**: - Extend the `Session` and `JWT` types using module augmentation - Run `pnpm tsc --noEmit` to check for type errors **Credentials provider not working**: - Ensure `session.strategy` is set to `"jwt"` - Check `authorize` function returns correct user object with `id` field - Verify password hashing/comparison logic ## Resources - **Official Docs**: https://authjs.dev - **GitHub**: https://github.com/nextauthjs/next-auth - **Discord Community**: https://discord.gg/nextauth - **Examples**: https://github.com/nextauthjs/next-auth/tree/main/apps/examples - **Provider List**: https://authjs.dev/getting-started/providers