You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在Next.js 15配置Auth.js 5时遇Cookie修改权限错误求助

Auth.js 5 + Next.js 15 集成错误:Cookies can only be modified in a Server Action or Route Handler

问题场景

集成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修改权限错误。

具体修改步骤

  1. 修改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 };
  }
}
  1. 优化auth.ts的authorize函数,只返回必要用户字段
    在验证密码通过后,不要返回完整的用户对象(避免泄露敏感信息如密码),只返回JWT需要的核心字段:
// 替换原return user;
return { id: user.id, email: user.email };
  1. 验证环境变量配置
    确保.env.local中正确设置了AUTH_SECRET,可以用openssl rand -hex 32生成一个安全的密钥。

验证修改

启动开发服务器,重新提交登录表单,此时signIn会在Server Action的上下文内完成Cookie设置,错误应该会消失。

内容的提问来源于stack exchange,提问作者Raheel Dharolia

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 20:25:54