Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DraftMode __prerender_bypass cookie SameSite LAX instead of None #49927

Open
1 task done
fromthemills opened this issue May 17, 2023 · 14 comments · May be fixed by #65858
Open
1 task done

DraftMode __prerender_bypass cookie SameSite LAX instead of None #49927

fromthemills opened this issue May 17, 2023 · 14 comments · May be fixed by #65858
Assignees
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team.

Comments

@fromthemills
Copy link

fromthemills commented May 17, 2023

Verify canary release

  • I verified that the issue exists in the latest Next.js canary release

Provide environment information

Operating System:
      Platform: darwin
      Arch: x64
      Version: Darwin Kernel Version 22.3.0: Thu Jan  5 20:53:49 PST 2023; root:xnu-8792.81.2~2/RELEASE_X86_64
    Binaries:
      Node: 18.7.0
      npm: 8.15.0
      Yarn: 1.22.19
      pnpm: N/A
    Relevant packages:
      next: 13.4.3-canary.1
      eslint-config-next: 13.1.6
      react: 18.2.0
      react-dom: 18.2.0
      typescript: 4.9.5

Which area(s) of Next.js are affected? (leave empty if unsure)

App directory (appDir: true)

Link to the code that reproduces this issue

https://github.com/fromthemills/nextjs-draft-bug

To Reproduce

Start the app npm run dev

Visit https://localhost:3000/api/draft

Check the set-cookie header in network tab.

Describe the Bug

When activating draft mode the __prerender_bypass cookie is set to SameSite=lax which prevents a visuals CMS to set the cookie as part of an iFrame. The old preview mode set the cookie to SameSite=None

Screenshot 2023-05-17 at 12 24 37

New cookie:

__prerender_bypass=...; Path=/; HttpOnly; SameSite=lax

Old cookie:

__prerender_bypass=...; Path=/; HttpOnly; Secure; SameSite=None

Expected Behavior

SameSite=None like the old cookie

Which browser are you using? (if relevant)

No response

How are you deploying your application? (if relevant)

No response

NEXT-1472

@fromthemills fromthemills added the bug Issue was opened via the bug report template. label May 17, 2023
@github-actions github-actions bot added the area: app App directory (appDir: true) label May 17, 2023
@scmx
Copy link

scmx commented May 22, 2023

I have the same problem with Storyblok CMS
With pages api route I used to do like this: https://www.storyblok.com/faq/next-js-preview-iframes
But with the new app routes I don't see any way of modifying or accessing the resulting Set-Cookie from draftMode().enable() since the response instance is something I create myself with new NextResponse(), new Response() or NextResponse.json().

But I did find a work-around by redirecting to the same page, read the cookie value and manually build a new `Set-Cookie` header to replace the cookie to change to `SameSite=None; Secure. EDIT: No, didn't work
// route handler enabling draft mode
import { draftMode } from 'next/headers';
 
export async function GET(request: NextRequest) {
    const cookie = request.cookies.get('__prerender_bypass');

    if (!cookie) {
        draftMode().enable()
        return NextResponse.redirect(new URL('/api/draft', request.url));
    }

    return new Response('Draft mode is enabled', {
        headers: {
            'Set-Cookie': `__prerender_bypass=${cookie?.value}; Path=/; HttpOnly; SameSite=None; Secure`
        }
    });
}

(similar to the working code I have, rewritten to mimic https://github.com/fromthemills/nextjs-draft-bug/blob/main/app/api/draft/route.ts)


EDIT: I did try with cookies() as well. But then Chrome blocks the cookie from being set because it returned SameSite=none without Secure for some reason.

import { cookies, draftMode } from 'next/headers';
...
const cookieStore = cookies();
const cookie = cookieStore.get('__prerender_bypass')!;
return new Response('Draft mode is enabled', {
    headers: {
        'Set-Cookie': `__prerender_bypass=${cookie?.value}; Path=/; HttpOnly; SameSite=None; Secure`
    }
});

EDIT: Oops, no, my work-around did not actually work. 🤦

@fblenkle
Copy link

fblenkle commented May 24, 2023

Maybe it helps, but I found a strange behaviour:

In an app-Dir API endpoint I activated draftMode().enable() and tried to write an additional cookie with data for preview (as I did before with __next_preview_data).
That cookie should have SameSite=none and Secure=true to work in headless CMS with iframe integration.

It costs me 8+ hours to find out that draftMode.enable() was responsible for changing my additional cookie!

res.cookies.set({
    name: PREVIEW_COOKIE,
    value: token,
    secure: true,
    httpOnly: true,
    sameSite: 'none',
  });
  
  console.log(res.cookies.get(PREVIEW_COOKIE));
  console.log(res.headers.get('Set-Cookie'));

console.log was as expected, but the browser never showed the Secure flag, so the cookie was not taken.

As I don't need (I hope) draftMode right now, I removed it and just use my custom preview cookie. If I have time I search the source code for the reason, not sure if it's a bug.

@edustef
Copy link

edustef commented Jun 8, 2023

I don't know if it's related but for me I'm having issues when trying to enable draft mode when using a redirect.

This code where it returns new Response like in NextJS docs works and the cookie is set correctly. However when I do a redirect instead of returning new Response then the page redirects correctly but there is no cookie set.

// route handler with secret and slug
import { draftMode } from "next/headers";
import { redirect } from "next/navigation";

export async function GET(request: Request) {
  // // Parse query string parameters
  const { searchParams } = new URL(request.url);
  const secret = searchParams.get("secret");
  const slug = searchParams.get("slug");
  console.log({
    secret,
    slug,
  });

  // // Check the secret and next parameters
  // // This secret should only be known to this route handler and the CMS
  if (secret !== "mysecret" || !slug) {
    return new Response("Invalid token", { status: 401 });
  }

  // Enable Draft Mode by setting the cookie
  draftMode().enable();

  // // Redirect to the path from the fetched post
  // // We don't redirect to searchParams.slug as that might lead to open redirect vulnerabilities
  // redirect(slug);
  return new Response("Draft mode is enabled");
}

@JarFrank
Copy link

I have the same experience @edustef, I'm using next js 13.4.9 and contentful as my CMS

@chigoKawa
Copy link

chigoKawa commented Jul 26, 2023

I had a similar issue accessing the cookies via an iframe. I tried this and it's working for now.

import { draftMode, cookies } from "next/headers";
  const cookieStore = cookies();
  const cookie = cookieStore.get("__prerender_bypass")!;
  cookies().set({
     name: "__prerender_bypass",
     value: cookie?.value,
     httpOnly: true,
     path: "/",
     secure: true,
     sameSite: "none",
   });
 redirect(searchParams.get("redirect") || "/");

@styfle styfle added the linear: next Confirmed issue that is tracked by the Next.js team. label Jul 26, 2023
@styfle styfle self-assigned this Jul 26, 2023
@styfle
Copy link
Member

styfle commented Jul 26, 2023

@edustef The redirect issue seems unrelated. That was fixed in #49965. You can try it out by upgrading Next.js by running npm install next@latest (or equivalent using your desired package manager).

@fromthemills Thanks for created the repo! However it only shows the Next.js api route and not the corresponding iframe code. Can you share that code so we can confirm that SameSite will fix it?

@fromthemills
Copy link
Author

@styfle I never made a reproduction with an iFrame. As setting up such an example was a bigger effort. I am working with a headless CMS which support preview mode using an iFrame and which I used to test the behaviour.

I confirmed that the cookie is now set with SameSite as None on non development environments. On the development server it is still set as as Lax as you can see here in the code. Is there a specific reason for setting this to Lax only on the development server? In my headless CMS I also point to localhost/api/preview as one of the preview urls. So now the cookie is set correctly on staging and production urls but not for local development.

@mediabeastnz
Copy link

mediabeastnz commented Nov 3, 2023

I'm using Storyblok and can't use Draft mode...

UPDATE: this seems to work. Basically using draftMode() but once it's set, grabbing the value and then change it's SameSite settings.

draftMode().enable();

const draft = cookies().get('__prerender_bypass')
    
cookies().set('__prerender_bypass', draft?.value, {
    httpOnly: true,
    sameSite: 'None',
    secure: true,
    path: '/',
});

This allows me to check isEnabled as you normally would!!!

@Christian-Schwarz2003
Copy link

Christian-Schwarz2003 commented Nov 13, 2023

also an issue with contentful but this fix works:

draftMode().enable();

  const draft = cookies().get("__prerender_bypass");
  const draftValue = draft?.value;
  if (draftValue) {
    cookies().set({
      name: "__prerender_bypass",
      value: draftValue,
      httpOnly: true,
      path: "/",
      secure: true,
      sameSite: "none",
    });
  }

@moarief
Copy link

moarief commented Nov 14, 2023

I had a similar issue accessing the cookies via an iframe. I tried this and it's working for now.

import { draftMode, cookies } from "next/headers";
  const cookieStore = cookies();
  const cookie = cookieStore.get("__prerender_bypass")!;
  cookies().set({
     name: "__prerender_bypass",
     value: cookie?.value,
     httpOnly: true,
     path: "/",
     secure: true,
     sameSite: "none",
   });
 redirect(searchParams.get("redirect") || "/");

Thansk for sharing and it works for the preview, but for some reason deleting the cookie for exiting the preview/draft mode does not delete the cookie?

Draft route.ts:

// route handler enabling draft mode

import { draftMode, cookies } from "next/headers";
import { redirect } from "next/navigation";

export async function GET(request: Request) {
  // Parse query string parameters
  const { searchParams } = new URL(request.url);

  const secret = searchParams.get("secret");
  let slug = searchParams.get("slug");

  // Check the secret and next parameters
  // This secret should only be known to this route handler and the CMS
  if (secret !== "...") {
    return new Response("Invalid token", { status: 401 });
  }

  // to prevet home page from not working in preview mode
  slug = slug === "" ? "home" : slug;

  // If the slug doesn't exist prevent draft mode from being enabled
  if (!slug) {
    return new Response("Invalid slug", { status: 401 });
  }

  draftMode().enable();

  const cookieStore = cookies();
  const cookie = cookieStore.get("__prerender_bypass")!;
  cookies().set({
    name: "__prerender_bypass",
    value: cookie?.value,
    httpOnly: true,
    path: "/",
    secure: true,
    sameSite: "none",
  });

  redirect(`/${slug}`);
}

Disabled-Draft route.ts:

import { cookies, draftMode } from "next/headers";
import { redirect } from "next/navigation";

export async function GET(request: Request) {
  // Parse query string parameters
  const { searchParams } = new URL(request.url);

  let slug = searchParams.get("slug");

  // to prevet home page from not working in preview mode
  slug = slug === "" ? "home" : slug;

  // If the slug doesn't exist prevent draft mode from being enabled
  if (!slug) {
    return new Response("Invalid slug", { status: 401 });
  }

  draftMode().disable();

  // this does not work
  cookies().delete("__prerender_bypass");
 
  // or this
  cookies().delete({
    name: "__prerender_bypass",
    httpOnly: true,
    path: "/",
    secure: true,
    sameSite: "none",
  });

  redirect(`/${slug}`);
}

It works if I restart the dev server:
image

But if I switch to the draft version https://localhost:3010/api/draft?secret={preview-token}&slug=not-published and then go back to the disabled-draft version https://localhost:3010/api/disabled-draft?slug=not-published I still get the draft version and not the published...

@moarief
Copy link

moarief commented Nov 15, 2023

Update

Got it working after some testing.

As previous, thanks to you all, to set the cookie use the following in a api/draft/route.ts:

const cookieStore = cookies();
const cookie = cookieStore.get("__prerender_bypass")!;
cookies().set({
  name: "__prerender_bypass",
  value: cookie?.value,
  httpOnly: true,
  path: "/",
  secure: true,
  sameSite: "none",
});

Then to exit the draft mode take the same cookie and set the expire data to past, api/disabled/route.ts

const cookieStore = cookies();
const cookie = cookieStore.get("__prerender_bypass")!;
cookies().set({
  name: "__prerender_bypass",
  value: cookie?.value,
  expires: new Date(0), // Set expiration date to the past
  httpOnly: true,
  path: "/",
  secure: true,
  sameSite: "none",
});

This solves the issue of updating the cookie in the CMS (my case Storyblok).

@soleo
Copy link

soleo commented Dec 14, 2023

I'd like to get a fix in place in nextjs if no one has been working on this issue on NextJS dev team side. The fix suggested above from above comment works fine, but I think it might be worth it to fix it on the framework side instead of copying a block of Cookie rewrite snippet in each application.

I don't think having SameSite=None; Secure is a good replacement for long run, and sharing cookies for third parties isn't a good idea either.

How about using events for cross origin domains when nextjs app is being embedded as iframes? @styfle I saw this issue is assigned to you. Do you mind to share some of your thought and is it possible for me to work on it if you haven't started looked into this issue?

@backflip
Copy link
Contributor

I have used the following workaround for the pages router:

// ...

res.setDraftMode({ enable: true });

const cookies = res.getHeader("Set-Cookie").map((cookie) => {
  if (cookie.match(/^__prerender_bypass=/)) {
    return cookie.replace(/SameSite=Lax/, "SameSite=None; Secure=True");
  }

  return cookie;
});

res.setHeader("Set-Cookie", cookies);

// ...

@joey-ma
Copy link

joey-ma commented Jan 19, 2024

Update

Got it working after some testing.

As previous, thanks to you all, to set the cookie use the following in a api/draft/route.ts:

const cookieStore = cookies();
const cookie = cookieStore.get("__prerender_bypass")!;
cookies().set({
  name: "__prerender_bypass",
  value: cookie?.value,
  httpOnly: true,
  path: "/",
  secure: true,
  sameSite: "none",
});

Then to exit the draft mode take the same cookie and set the expire data to past, api/disabled/route.ts

const cookieStore = cookies();
const cookie = cookieStore.get("__prerender_bypass")!;
cookies().set({
  name: "__prerender_bypass",
  value: cookie?.value,
  expires: new Date(0), // Set expiration date to the past
  httpOnly: true,
  path: "/",
  secure: true,
  sameSite: "none",
});

This solves the issue of updating the cookie in the CMS (my case Storyblok).

thanks for sharing! just to point out this works for Contentful as well.

@styfle styfle linked a pull request May 16, 2024 that will close this issue
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: app App directory (appDir: true) bug Issue was opened via the bug report template. linear: next Confirmed issue that is tracked by the Next.js team.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

14 participants