Search code examples
next.jsnext-authauth.js

AuthJS 5 with Next JS 15 - Error: Cookies can only be modified in a Server Action or Route Handler


Having Error: Cookies can only be modified in a Server Action or Route Handler while trying to setup Auth.js with Next 15, would really appreciate if someone may help.

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;

          // Look for user in DB
          user = await getUserByEmail(email);

          // User does not exist with that email
          if (!user) {
            console.log("Error: Email does not exist");
            throw new Error("Invalid credentials.");
          }

          // User Exists but password in DB is null  for oAuth (Google,Github) login
          if (!user.password) {
            console.log("Error: OAuth case here");
            throw new Error("oAuth case");
          }

          // Match Password
          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. Define your form.
  const form = useForm<SigninInput>({
    resolver: valibotResolver(SigninSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  });

  // 2. Define a submit handler.
  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"
  }
}

Trying to setup Auth.JS 5 with Next JS 15, error generates in auth.ts when returning the user to set cookies.


Solution

  • Instead of calling a server action to sign in, import signIn method from next-auth/react and run (please change the html elements in the example with their corresponding equivalents, I keep it light for better presentation)

    "use client"
    
    import { signIn } from "next-auth/react"
    
    export default function SignInForm() {
        const signInAction = (formData: FormData) => {
            signIn("credentials", formData)
        }
    
        return (
            <form
                onSubmit={signInAction}
            >
                <label htmlFor="email">
                    Email
                    <input type="email" id="email" name="email" />
                </label>
                <label htmlFor="password">
                    Password
                    <input name="password" id="password" type="password" />
                </label>
                <button>Submit</button>
            </form>
        )
    }