authjs-skills

安装量: 318
排名: #2905

安装

npx skills add https://github.com/gocallum/nextjs16-agent-skills --skill authjs-skills

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 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

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

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):

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.

  1. Create API Route Handler

Create app/api/auth/[...nextauth]/route.ts:

import { handlers } from "@/auth"

export const { GET, POST } = handlers

  1. Add Middleware (Optional but Recommended)

Create middleware.ts at the project root:

export { auth as middleware } from "@/auth"

export const config = { matcher: ["/((?!api|_next/static|_next/image|favicon.ico).*)"], }

For more control:

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 Go to Google Cloud Console Create a new project or select existing Enable Google+ API 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 Copy Client ID and Client Secret to .env.local 2. Configuration 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" } } }), ], })

  1. Google Provider Options 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

Install required packages for credentials provider

pnpm add bcryptjs zod pnpm add -D @types/bcryptjs

  1. Basic Configuration 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 }, })

  1. User Registration Example // 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 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 "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) // app/providers.tsx "use client"

import { SessionProvider } from "next-auth/react"

export function Providers({ children }: { children: React.ReactNode }) { return {children} }

// app/layout.tsx import { Providers } from "./providers"

export default function RootLayout({ children, }: { children: React.ReactNode }) { return ( {children} ) }

// 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 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 // app/auth/signin/page.tsx import { signIn } from "@/auth"

export default function SignInPage() { return (

Sign In

  {/* 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>

) }

Sign Out import { signOut } from "@/auth"

export default function SignOutButton() { return (

{ "use server" await signOut() }} >
) }

Session Management Session Strategy

Auth.js v5 supports two session strategies:

JWT (Default): Stores session in encrypted JWT token Database: Stores session in database 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 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 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:

pnpm add @auth/prisma-adapter

Then configure it in your auth.ts:

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:

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 // 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 // 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: 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 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 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 // 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 */}
    <div className="space-y-4">
      <form
        action={async () => {
          "use server"
          await signIn("google", { redirectTo: callbackUrl })
        }}
      >
        <button 
          type="submit"
          className="w-full bg-white border border-gray-300 text-gray-700 py-2 px-4 rounded hover:bg-gray-50"
        >
          Continue with Google
        </button>
      </form>
    </div>

    <div className="relative">
      <div className="absolute inset-0 flex items-center">
        <div className="w-full border-t border-gray-300" />
      </div>
      <div className="relative flex justify-center text-sm">
        <span className="bg-white px-2 text-gray-500">Or</span>
      </div>
    </div>

    {/* Credentials Form */}
    <form
      action={async (formData) => {
        "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"
    >
      <div>
        <label htmlFor="email" className="block text-sm font-medium text-gray-700">
          Email
        </label>
        <input
          id="email"
          name="email"
          type="email"
          required
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>
      <div>
        <label htmlFor="password" className="block text-sm font-medium text-gray-700">
          Password
        </label>
        <input
          id="password"
          name="password"
          type="password"
          required
          className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"
        />
      </div>
      <button
        type="submit"
        className="w-full bg-blue-600 text-white py-2 px-4 rounded hover:bg-blue-700"
      >
        Sign In
      </button>
    </form>
  </div>
</div>

) }

Role-Based Access Control (RBAC) // 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 Import changes: next-auth package remains the same, but imports are simplified Universal auth(): Replace getServerSession with auth() Middleware: Use auth as middleware directly Configuration: More streamlined, fewer options needed Migration Steps // 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() }

// 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

返回排行榜