在Next.js 15配置Auth.js 5时遇Cookie修改权限错误求助
问题场景
集成Auth.js 5(v5.0.0-beta.25)与Next.js 15(v15.0.3)时,触发错误提示:Cookies can only be modified in a Server Action or Route Handler,错误发生在auth.ts中返回用户以设置Cookie的环节。当前采用JWT会话策略,客户端表单提交后通过Server Action调用signIn方法触发该错误。
相关代码
auth.ts
import NextAuth from "next-auth"; import Credentials from "next-auth/providers/credentials"; import * as v from "valibot"; import { SigninSchema } from "@/validators/signin-validator"; import { getUserByEmail } from "./functions/user"; import * as argon2 from "argon2"; const nextAuth = NextAuth({ session: { strategy: "jwt" }, secret: process.env.AUTH_SECRET, pages: { signIn: "/auth/signin" }, providers: [ Credentials({ async authorize(credentials) { let user = null; const parsed = v.safeParse(SigninSchema, credentials); if (parsed.success) { const { email, password } = parsed.output; // 从数据库查找用户 user = await getUserByEmail(email); // 用户不存在 if (!user) { console.log("Error: Email does not exist"); throw new Error("Invalid credentials."); } // 用户存在但密码为空(OAuth登录用户) if (!user.password) { console.log("Error: OAuth case here"); throw new Error("oAuth case"); } // 验证密码 const passwordMatch = await argon2.verify(user.password, password); if (passwordMatch) { return user; } return null; } else { return null; } }, }), ], }); export const { handlers, auth, signIn, signOut } = nextAuth;
actions/signin.ts
"use server"; import { signIn } from "@/auth"; type Res = | { success: true } | { success: false; error: string; statusCode: 500 }; export async function signinAction(values: unknown): Promise<Res> { // Auth logic will be done in our AuthJS Config files try { if ( typeof values !== "object" || values === null || Array.isArray(values) ) { throw new Error("Invalid JSON object"); } signIn("credentials", { ...values, redirect: false }); return { success: true }; } catch (err) { return { success: false, error: "Internal Server Error", statusCode: 500 }; } }
signin-form.tsx
"use client"; import { useForm } from "react-hook-form"; import { valibotResolver } from "@hookform/resolvers/valibot"; import { type SigninInput, SigninSchema } from "@/validators/signin-validator"; import { Button } from "@/components/ui/button"; import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { signinAction } from "@/actions/signin"; export default function SigninForm() { //const [success, setSuccess] = useState(false); // 1. 定义表单 const form = useForm<SigninInput>({ resolver: valibotResolver(SigninSchema), defaultValues: { email: "", password: "", }, }); // 2. 定义提交处理函数 async function onSubmit(values: SigninInput) { const res = await signinAction(values); console.log(res); } return ( <> <Form {...form}> <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 max-w-[400px] mx-auto" autoComplete="false"> <FormField control={form.control} name="email" render={({ field }) => ( <FormItem> <FormLabel>Email</FormLabel> <FormControl> <Input placeholder="john@doe.com" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <FormField control={form.control} name="password" render={({ field }) => ( <FormItem> <FormLabel>Password</FormLabel> <FormControl> <Input type="password" placeholder="*****" {...field} /> </FormControl> <FormMessage /> </FormItem> )} /> <Button type="submit" disabled={form.formState.isSubmitting} className="w-full"> Submit </Button> </form> </Form> </> ); }
package.json
{ "name": "giraffe-auth", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev --turbopack", "build": "next build", "start": "next start", "lint": "next lint", "db:generate": "drizzle-kit generate", "db:migrate": "drizzle-kit migrate", "db:studio": "drizzle-kit studio" }, "dependencies": { "@hookform/resolvers": "^3.9.1", "@neondatabase/serverless": "^0.10.3", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-slot": "^1.1.0", "argon2": "^0.41.1", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "drizzle-orm": "^0.36.3", "lucide-react": "^0.456.0", "next": "15.0.3", "next-auth": "^5.0.0-beta.25", "react": "19.0.0-rc-66855b96-20241106", "react-dom": "19.0.0-rc-66855b96-20241106", "react-hook-form": "^7.53.2", "server-only": "^0.0.1", "tailwind-merge": "^2.5.4", "tailwindcss-animate": "^1.0.7", "valibot": "^1.0.0-beta.3" }, "devDependencies": { "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", "drizzle-kit": "^0.28.1", "eslint": "^8", "eslint-config-next": "15.0.3", "postcss": "^8", "tailwindcss": "^3.4.1", "typescript": "^5" } }
解决方案
核心问题原因
Next.js 15对Cookie操作的权限控制更严格,要求所有Cookie修改必须在Server Action或Route Handler的同步上下文内完成。你的代码中signIn是异步函数,但在signinAction里没有用await调用,导致signIn的异步操作脱离了Server Action的上下文,从而触发Cookie修改权限错误。
具体修改步骤
- 修改
actions/signin.ts,添加await并正确处理登录结果
"use server"; import { signIn } from "@/auth"; type Res = | { success: true } | { success: false; error: string; statusCode: number }; export async function signinAction(values: unknown): Promise<Res> { try { if ( typeof values !== "object" || values === null || Array.isArray(values) ) { return { success: false, error: "无效的输入格式", statusCode: 400 }; } // 必须await signIn,确保在Server Action上下文内完成Cookie操作 const result = await signIn("credentials", { ...values, redirect: false }); // 处理Auth.js返回的错误信息 if (result?.error) { return { success: false, error: result.error, statusCode: 401 }; } return { success: true }; } catch (err) { return { success: false, error: "内部服务器错误", statusCode: 500 }; } }
- 优化
auth.ts的authorize函数,只返回必要用户字段
在验证密码通过后,不要返回完整的用户对象(避免泄露敏感信息如密码),只返回JWT需要的核心字段:
// 替换原return user; return { id: user.id, email: user.email };
- 验证环境变量配置
确保.env.local中正确设置了AUTH_SECRET,可以用openssl rand -hex 32生成一个安全的密钥。
验证修改
启动开发服务器,重新提交登录表单,此时signIn会在Server Action的上下文内完成Cookie设置,错误应该会消失。
内容的提问来源于stack exchange,提问作者Raheel Dharolia
相关产品推荐
相关产品推荐

