Next.js Discord

Discord Forum

http-only cookies problem

Answered
Elm sawfly posted this in #help-forum
Open in Discord
Elm sawflyOP
Im testing out jwt tokens recieved from a standalone express server into my nextjs "frontend",
i can recieve (consol.log) out the cookie from the request in the frontend, but when i send requests from the frontend the cookie is not there.
Answered by Ray
you need to add the header into the fetch, the cookies is set on the browser, the server doesn't have information to it
import { headers } from 'next/headers'

fetch("http://localhost:8080/api/auth/refresh-token", {
  headers: new Headers(headers()),
});
View full answer

67 Replies

Elm sawflyOP
some snippets of my problem:

// Part where cookie is created on login endpoint in express:
console.log("User signed in: ", user);
    res
      .status(200)
      .cookie("refreshToken", refreshToken, {
        httpOnly: true,
        sameSite: "strict",
        // secure: process.env.NODE_ENV === "production",
        path: "/",
      })
      .header("Authorization", accessToken)
      .send({ ok: true, accessToken });
  } catch (error: any) {
    res.status(400).send({
      ok: false,
      error: error.message,
    });
  }


// login function in nextjs
export async function login(formData: FormData) {
  // console.log(formData);
  let payload: Record<string, string> = {};
  for (const [key, value] of formData.entries()) {
    payload[key] = value as string;
  }
  const response = await fetch("http://localhost:8080/api/login", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
  });
  if (!response.ok) {
    console.log("Error");
    throw new Error("Failed to sign in: ", response as any);
  }
  const refreshToken = response.headers.get("set-cookie");
  console.log("Refresh Token:", refreshToken);
  const data = await response.json();
  const { accessToken } = data;
  console.log(data, "(AUTHLIB)"); // This consol log outputs the refreshToken in the response


  cookies().set("accessToken", accessToken, {
    path: "/",
    sameSite: "strict",
    secure: true,
  });
}
// consol log outputs this:
//Refresh Token: refreshToken=refreshtokengibberish; Path=/; HttpOnly; SameSite=Strict
//{
//  ok: true,
//  accessToken: 'jwttokengibberish'
//} (AUTHLIB)
All fine and dandy so far, but from here i dont understand.. i must be doing something wrong

// here i try to send a request back to the backend in express with the refreshToken 
  try {
    const response = await fetch(
      "http://localhost:8080/api/auth/refresh-token",
      {
        method: "POST",
        credentials: "include", // Include cookies in the request
      }
    );

    if (!response.ok) {
      throw new Error("Failed to refresh access token");
    }

    const data = await response.json();


But when my backend process the request like this:

   const refreshToken = req.cookies.refreshToken; // Retrieve the refresh token from the HTTP-only cookie
    console.log("Hi im in refreshing function", refreshToken);
    console.log(req.cookies);

i get this response:

Hi im in refreshing function undefined
[Object: null prototype] {}


and i cant figure out why its not including the tokens?
note, if i set the refreshtoken as a cookie on frontend like i do with the access token it works, but i dont want the cookie in the DOM, i want it in the requests only
Labrador Duck
You're not storing refreshToken anywhere so it's lost. HTTP is a stateless protocol so you will have to store it somewhere
Elm sawflyOP
i see, i was storing it as i did with access token before and then it worked, however i dont want the DOM to access it so i should do something like this in frontend?:

  cookies().set("accessToken", accessToken, {
    path: "/",
    sameSite: "strict",
    secure: true,
  });

  cookies().set("refreshToken", refreshToken, {
    path: "/",
    sameSite: "strict",
    httpOnly: true,// i added this line
    secure: true,
  });


or am missunderstanding?
Labrador Duck
yes http only cookies won't be accessible to the dom and js client
Elm sawflyOP
thanks a million @Labrador Duck, i thought the http-only cookie magically was stored in browser somehow and i shouldnt store it with the frontend code :p
Labrador Duck
add httpOnly: true in accessToken too ,
Elm sawflyOP
yes, like the example in last snippet right?
Labrador Duck
yes
Elm sawflyOP
perfect :thank_you:
Labrador Duck
I see you're not setting expiry so all these will be sessio cookies
Elm sawflyOP
the jwttoken has expiry in them
Labrador Duck
what the duration of jwt?
Elm sawflyOP
maybe i should decode that expiry and add it to cookie aswell?
right now its just testing, so 1 minute on accesstoken and 1hour on refresh
i know that the accessToken gets expired after 1 minute, however the cookie is still in browser but invalid. maybe its bad practice?
{
  "id": 1,
  "roles": [
    "USER"
  ],
  "iat": 1712423547,
  "exp": 1712423607
}
thats whats in my tokens atm
Labrador Duck
expiry is for lower time so you can get away with it but if user closes the browser and again wants to access the app within an hour then they may need to reauthenticate
setting expiry will remove those cookie value once expired
Answer
Elm sawflyOP
cool thank you, now i have just a final issue in this subject hehe
i get error about updating the cookie with new token:
Failed to refresh access token: td [Error]: Cookies can only be modified in a Server Action or Route Handler. 
i understand it has todo with streaming data and cookie creation issues?
Elm sawflyOP
sec ill give you snippets
export async function getSession() {
  const session = cookies().get("accessToken")?.value;
  // If no accessToken exists, return null
  if (!session) return null;
  // If accessToken exists but has expired, attempt to refresh it using the refreshToken
  if (isTokenExpired(session)) {
    console.log("Access token expired. Refreshing...");
    // console.log(cookies().getAll());
    const creds = cookies().getAll();
    try {
      // Attempt to update the session
      await updateSession();
      // Get the updated session after refreshing
      const updatedSession = cookies().get("accessToken")?.value;
      return updatedSession || null;
    } catch (error) {
      console.error("Failed to refresh session:", error);
      return null; // Return null if refresh fails
    }
  }
  // Return the session if it's not expired
  return session;
}



export async function updateSession() {
  console.log(cookies().getAll());
  try {
    const response = await fetch(
      "http://localhost:8080/api/auth/refresh-token",
      {
        method: "POST",
        headers: new Headers(headers()),
      }
    );
    if (!response.ok) {
      // console.log(response);
      throw new Error("Failed to refresh access token");
    }

    const data = await response.json();
    const newAccessToken = data.accessToken;
 

    // await updateCookie(newAccessToken); 
  } catch (error) {
    console.log("Failed to refresh access token:", error);
    throw error; // Rethrow the error for handling in getSession()
  }
}
its a bit messy, but ive been struggeling to grasp this hehe i will write it better when its working 🙂
you see where i commented //await updateCookie, i tried setting cookie there before i tried that aproach
and are you calling getSession on the page component?
Elm sawflyOP
i call it like this
export default async function Messages() {
  const accessToken = await getSession();
as i use access token in authorization headers
yeah, cookies cannot be set from the page
Elm sawflyOP
how would you recommend i do
do it in middleware
Elm sawflyOP
i was actually trying that before you wrote 🙂 but i messed it up
post your code
headers() and cookies() function doesn't work in middleware, use req.headers and req.cookies instead
Elm sawflyOP
hehe sec its nothing to show as its not working 🤣
export async function middleware(request: NextRequest) {
  let response = NextResponse.next();
  console.log(response.cookies);
  return;
  response;
}

didint get any further, was trying to understand how it works
but middleware is the only way to go?
@Elm sawfly hehe sec its nothing to show as its not working 🤣 export async function middleware(request: NextRequest) { let response = NextResponse.next(); console.log(response.cookies); return; response; } didint get any further, was trying to understand how it works
import { NextRequest, NextResponse } from "next/server";

export async function middleware(req: NextRequest) {
  const session = req.cookies.get("accessToken")?.value;
  if (isTokenExpired(session)) {
    const response = await fetch(
      "http://localhost:8080/api/auth/refresh-token",
      {
        method: "POST",
        headers: new Headers(req.headers),
      }
    );
    if (!response.ok) {
      // console.log(response);
      console.log("Failed to refresh access token");
      const res = NextResponse.next();
      res.cookies.delete("accessToken");
      return res;
    }

    const data = await response.json();
    const newAccessToken = data.accessToken;

    const res = NextResponse.next();
    res.cookies.set({
      name: "accessToken",
      value: newAccessToken,
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
    });
    return res;
  }
}
Elm sawflyOP
ahh i see, so should i then remove the updateSession?
you can't see cookies from the page
Elm sawflyOP
interresting
so hmm, my getSession used updateSession. can i force it to trigger middleware?
@Elm sawfly so hmm, my getSession used updateSession. can i force it to trigger middleware?
how do you force it to trigger middleware?
Elm sawflyOP
im not sure but middleware needs response/request from somewhere?
every request go through the middleware first
Elm sawflyOP
im very tired, been working daytime and tried fixing this entire evening 😄
oh so wait, so since middleware triggers on every request i can strip this down abit?
export async function getSession() {
  const session = cookies().get("accessToken")?.value;
  // If no accessToken exists, return null
  if (!session) return null;
  // If accessToken exists but has expired, attempt to refresh it using the refreshToken
  if (isTokenExpired(session)) {
    console.log("Access token expired. Refreshing...");
    // console.log(cookies().getAll());
    const creds = cookies().getAll();
    try {
      // Attempt to update the session
      await updateSession();
      // Get the updated session after refreshing
      const updatedSession = cookies().get("accessToken")?.value;
      return updatedSession || null;
    } catch (error) {
      console.error("Failed to refresh session:", error);
      return null; // Return null if refresh fails
    }
  }
  // Return the session if it's not expired
  return session;
}
Elm sawflyOP
pretty much like this i suppose
export async function getSession() {
  const session = cookies().get("accessToken")?.value;
  // If no accessToken exists, return null
  if (!session) return null;
  // Return the session if it's not expired
  return session;
}
Elm sawflyOP
oh even better, really appriciate your help on this
just 1 minor flaw, but i think ill fix it. problem is middleware deleted my accessToken, and it broke my signin as middleware triggers first haha
yeah because of this
if (!response.ok) {
      // console.log(response);
      console.log("Failed to refresh access token");
      const res = NextResponse.next();
      res.cookies.delete("accessToken");
      return res;
    }
you have to use matcher to config when should the middleware run
export const config = {
  matcher: [
    /*
     * Match all request paths except for the ones starting with:
     * - api (API routes)
     * - _next/static (static files)
     * - _next/image (image optimization files)
     * - favicon.ico (favicon file)
     */
    '/((?!api|_next/static|_next/image|favicon.ico).*)',
  ],
}
otherwise static file request will also trigger the middleware
that would cause the fetch failed
Elm sawflyOP
what if in the future i would want multiple middleware functions, would that matcher trigger on all of them or can you specifty them individually
wierd question, but like 1 matcher per middleware function
Elm sawflyOP
thanks a lot for your inputs. it helped me understand this more, im pretty new in next/react/js.
have a nice weekend!