Setting a cookie in nextjs 14
Asked Answered
G

7

14

I have this nextjs app where i'm trying to setup my authentication/authorization system, it's pretty simple, i have a refresh token (long duration) which i store in a httpOnly cookie, and i have also an access token (short duration) in them cookies (at first, i wanted to store my access token as a Bearer access-token header, but, setting that header up in server components and the client side got me crying for almost a week since it is impossible).

So, if my access-token cookie expires, i need to call my api (this is where problem shows up) using this endpoint (/access-token) that generates the new token, and sets the cookie, but, if i'm calling this endpoint in a server component the cookie won't be set in my browser, and i understand that since a server component is running on the server side and not on client side.

Then i said, aaight cool, i'ma set the cookie in my nextjs app, so, when i call the same endpoint, i return the access token and set it using import {cookies} from 'next/headers', but it didn't work since server components can not have side effects for cache and design purposes (and things of that nature).

Then i go to the docs, where they say that if i want to set a cookie, i need a server action or a route handler, and this is what i did.

My server component (home page /) πŸ‘‡


const fetchUser = async (): Promise<HomeData | undefined> => {
  await getAccessToken("aaight");
  // await axios.post(
  //   "http://localhost:3000/api/set-cookie",
  //   {},
  //   {
  //     headers: {
  //       Cookie: cookies().toString(),
  //     },
  //     timeout: 5000,
  //   }
  // );

  const refreshToken = cookies().get("refresh-token");
  // console.log("refresh token", refreshToken);

  if (!refreshToken) {
    return undefined;
  }

  console.log("\ncookie set\n", cookies().getAll());

  // get user
  const getUser = await foodyApi.get<SuccessfullResponse<{ user: User }>>(
    "/users/me",
    {
      headers: {
        Cookie: cookies().toString(),
      },
      timeout: 10000,
    }
  );


  return {
    user: getUser.data.rsp.user,
  };
};

Server action (/app/action.ts) πŸ‘‡ (this is the first fn that runs in my server component because i need to do this types of operations before streaming starts)


"use server";

import { cookies } from "next/headers";

export const getAccessToken = async (token: string) => {
  cookies().set("access-token", token, {
    path: "/",
    domain: "localhost",
    maxAge: 300,
    httpOnly: true,
    secure: false,
  });
};

This also didn't work, and i got an error ❌ that says.

`

β¨― Error: Cookies can only be modified in a Server Action or Route Handler. Read more: https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options
at getAccessToken (./app/action.ts:15:61)
at fetchUser (./app/page.tsx:20:66)
at Home (./app/page.tsx:51:24)
at async Promise.all (index 0)

`

Then i try with an api route handler πŸ‘‡ (/app/api/set-cookie)


import { cookies } from "next/headers";

export const POST = async (req: Request) => {
  // FIXME: check we got refresh token and api key

  console.log(cookies().get("access-token"));

  cookies().set("access-token", "we did it");

  return Response.json(
    {},
    { status: 200 }
  );
};

That didn't work as well.

It's been a week for me trying to setup a normal auth system for my app, and i've changed things about my system so that i can use nextjs, but i'm having problems again, how am i supposed to set my cookie now ?ΒΏ redirect to another page with use client mark and set the cookie then redirect here to my page with the server component ?ΒΏ that work around sounds horrible, and i don't know about user experience man...

Glidden answered 31/10, 2023 at 13:57 Comment(2)
as I know, cookies() works with server-actions and Route handlers, but I have the same problem – Haversine
@HamidMohamadi check my answer below. or you can also find my repo that is working fine. – Forcefeed
T
5

This approach is the right one

import { cookies } from "next/headers";

export const POST = async (req: Request) => {
  // FIXME: check we got refresh token and api key

  console.log(cookies().get("access-token"));

  cookies().set("access-token", "we did it");

  return Response.json(
    {},
    { status: 200 }
  );
};

But you have to update the cookie on the response:

    export const POST = async (req: NextRequest) => {  
      const response = Response.json(
        {},
        { status: 200 }
      );

      response.cookies.set("access-token", "we did it");

      return response;

    };
Teddman answered 18/11, 2023 at 22:53 Comment(1)
Yep, using a route-handler – Whitish
S
5

Example of using middleware in Next JS

If, for instance, you are using middleware, you can create a variable response instead of using the response from parameter, assign it to NextResponse.next(), set the cookies, and finally return the response.

import { NextResponse, NextRequest } from 'next/server'

export function middleware(request: NextRequest) {

    const accessToken = request.cookies.get('access-token')?.value
    
    console.log( accessToken )

    const response = NextResponse.next()

    response.cookies.set('access-token', 'Your secret token')

    return response
}
Sfax answered 21/5 at 21:57 Comment(0)
M
3

This works :


import axios from "axios";

import { redirect } from "next/navigation";
import { cookies } from "next/headers";
import { revalidatePath } from "next/cache";

const Page = async ( ) => {
    const getCSRFToken = async () => {
        "use server";
        try {
          const {data, headers} = await axios.get( "http://backend:8000/api/auth/csrf/");
          const csrfToken = data.csrf_token;
          // or get it from headers['set-cookie'] but this needs to be parsed into object in specific form accepted by next.js cookie() api 
          // and also you need to make sure option keys are camelCase

          // Set CSRF token as a cookie
          cookies().set('csrftoken', csrfToken,  { httpOnly: true })
          return csrfToken;
        } catch (error) {
          console.error("Error fetching CSRF token:", error);
          return null;
        }
      };
    return (
        <div>
          <section>
            <form
              action={async (formData) => {
                "use server";
                await getCSRFToken()
                revalidatePath('/test-api')
                redirect("/");
              }}
            >
              {/* <input type="email" placeholder="Email" /> */}
              <br />
              <button type="submit">Login</button>
            </form>
            
          </section>
          </div>
    )
};

export default Page;
Maigre answered 4/4 at 20:41 Comment(0)
F
2

I had a similar problem but I solved it.

In my case, the cookie was set by lucia & I had to do sessionCookie.serialize() like:

return new Response(null, {
    status: 302,
    headers: {
      Location: '/',
      'Set-Cookie': sessionCookie.serialize(),
    },
  })

But I wanted to use redirect from next/navigation which doesn't allow options like above so I set it like:

  const session = await lucia.createSession(userId, {})
  const sessionCookie = lucia.createSessionCookie(session.id)
  cookies().set(sessionCookie)

  return redirect('/')

In another place, I used regular cookies().set() method like:

import { TimeSpan } from 'oslo' // or lucia. both works.

cookies().set(key, 'true', {
    maxAge: new TimeSpan(10, 'm').seconds(), // 10 minutes = 60 * 60 * 1
  })

Note that this is in a server action.

Forcefeed answered 29/1 at 6:47 Comment(2)
I am running into this exact same issue but unfortunately the above hasn't fixed it for me – Sitton
Here's what ended up working for me inside a next js app router 14 route using lucia to create the cookie: const sessionCookie = await createSessionCookie({ userId }); const res = NextResponse.redirect( new URL(REDIRECT_URL_AFTER_SIGN_IN, process.env.WEB_URL) ); res.cookies.set(sessionCookie.name, sessionCookie.value); – Sitton
H
2

Your understanding of the limitation is correct. In Next.js, due to the streaming feature of server components, cookies cannot be directly set within them. This design decision by Next.js helps to prevent errors related to unintended side effects during server-side rendering.

For more details, you may refer to the Next.js documentation on rendering server components: https://nextjs.org/docs/app/building-your-application/rendering/server-components.

To summarize, for cookies to be set, the rendering process on the server needs to be complete. Due to Next.js's architecture, a Server Action is typically invoked from a client component. If you need to perform this operation within a server component, consider creating a client-side component like this:

'use client';

import { useEffect } from 'react';

export default function ServerAction({ action }: { action: () => void }) {
  useEffect(() => {
    action();
  }, []);

  return <></>;
}

Then, on the server side, you can invoke this component and pass the action you want to perform as an argument. This approach allows you to work around the limitation by leveraging client-side execution to trigger server actions, ensuring that cookies can be set following the completion of server-side rendering.

Humdinger answered 5/3 at 20:14 Comment(0)
S
2

Cookies can only be modified in a Server Action or Route Handler (Read More).

Modifying cookies in the Route Handler can be a little confusing.

Server Action

async function doSomethingAction() {
  "use server";
  cookies().set("action", "true");
  return { success: true };
}

Route Handler (Two approaches)

export async function GET(req: Request) {
    // 1. Request from the server component
    cookies().set("handler", "true");
    // 2. Request from the client component
    return Response.json(
        {
            something: 1,
        },
        {
            headers: {
                "Set-Cookie": "handler=true; Path=/; HttpOnly; SameSite=Strict;",
            },
        },
    );
}

Samora answered 8/5 at 9:14 Comment(0)
D
1

You have to add "use server" in actions.ts then get cookies and set a new header, in this example I work with external backend and its working its a js file:

"use server"
import { cookies } from 'next/headers'
import { NextResponse } from "next/server"

export  async function GETCSRF() {
      const res = await fetch(`http://localhost:3500/get-csrf-token`,{
        credentials:"include",
        headers:{
          "content-type":"application/json"  },
          cache:"no-store"
       }) 
       const cookiesList = cookies()
  
       const cookieStore = res.headers;
       const setCookieHeader = cookieStore.get('set-cookie');
       
       if(setCookieHeader){
       const cookiesArray = setCookieHeader.split(/[;,]/);
       
       let csrfToken;
       let _csrf;
       
       console.log('setCookieHeader:', setCookieHeader);
       
       for (const cookie of cookiesArray) {
         const [name, value] = cookie.trim().split('=');
       
         if (name === '_csrf') {
           _csrf = value;
         }
         if (name === 'csrfToken') {
           csrfToken = value;
         }
       }
       
       console.log('Extracted _csrf:', _csrf);
       console.log('Extracted csrfToken:', csrfToken);
       
       cookiesList.set({
           name: 'csrfToken',
           value: csrfToken,
           Secure:true,
           httpOnly: true,
           path: '/',
         })
         cookiesList.set({
           name: '_csrf',
           value: _csrf,
           Secure:true,
           httpOnly: true,
           path: '/',
         })
       
        }
const data=await res.json()
console.log("hi from csrf route");

// return Response.json({data})
return NextResponse.json({ data: data }, { status: 200})
} 

I hope this can help you!!

Desperation answered 23/11, 2023 at 9:50 Comment(0)

© 2022 - 2024 β€” McMap. All rights reserved.