apollographql / apollo-client-nextjs

Apollo Client support for the Next.js App Router
https://www.npmjs.com/package/@apollo/experimental-nextjs-app-support
MIT License
358 stars 25 forks source link

useSuspenseQuery not sending cookies on Server Side query #85

Open ODreelist opened 9 months ago

ODreelist commented 9 months ago

Hi there,

I may be missing something simple here but I have an apollo-provider that is pretty standard, however we use supabase auth and the session is set in cookies.

Obviously the cookie is present when the user refreshes the query page, but its not until the client re-initiates the query (which I don't think would happen if this issue was solved) that the server sees the cookies passed. The httpLink and client set up is as follows:

` const isServer = typeof window === "undefined"; const httpLink = new HttpLink({ uri: GRAPHQL_URI, credentials: "include" });

return new NextSSRApolloClient({ cache: apolloCache, link: isServer ? ApolloLink.from([new SSRMultipartLink({ stripDefer: true }), httpLink]) : httpLink, }); }; `

I know the cookie is available because I can set it as an authorization header which does get fired in the query on the server side but I'd rather not have to "hack" it that way, Is there a way to ensure that even when query is processed on the server side that it includes the cookies?

Thanks in advance.

plausible-phry commented 8 months ago

The problem is that the server does per default not have any connection between cookies of the incoming request (browser to Next server) and outgoing requests (server to GraphQL server).

You will have to extract the cookie from the incoming request and then manually pass it on.

Unfortunately, in Next.js, Client Components cannot access the cookies of the incoming request - only server components can. So you need to:

ODreelist commented 8 months ago

I appreciate the thoughtful response, that's essentially my current implementation, except I manually add the token from the cookie to the HttpLink via headers.authorization. Is that what you mean? Or is there a way to actually manually add the cookie to the HttpLink that I'm not aware of.

plausible-phry commented 8 months ago

Is that what you mean?

I'd pass it into the HttpLink constructor via headers.authorization option, so probably what you are doing now :)

rval commented 7 months ago

pass it as props to a Client Component (your ApolloProviderWrapper)

Forgive me if I'm missing something obvious, but if you have an HTTP-only cookie that holds a sensitive credential, and then pass it to a client component, don't you risk exposing it to client-side code and introducing an XSS vulnerability?

Is the thinking here that unless you're actually rendering the cookie value it shouldn't show up in any part of the browser payload? Edit: looks like props passed to client components can show up in the browser payload, even if they're not used directly:

self.__next_f.push([1,"c:I{\"id\":\"(app-client)/./src/app/org/Name.tsx\",\"chunks\":[\"app/org/page:static/chunks/app/org/page.js\"],\"name\":\"\",\"async\":false}\nb:[[\"$\",\"h1\",null,{\"children\":[\"Hello, \",\"Michael Bluth\",\"!\"]}],[\"$\",\"$Lc\",null,{\"secret\":\"MY_SUPER_SECRET_VALUE\"}]]\n"])
phryneas commented 7 months ago

@rval I'd love to give you a better answer, but this seems like an architectural oversight on the side of Next.js - we won't be able to give you any better advice until they come up with a better way of doing this.

Maybe open an issue over there, and if they come up with something better, please report back here? I'd love to hear about that :)

Stevemoretz commented 7 months ago

pass it as props to a Client Component (your ApolloProviderWrapper)

Forgive me if I'm missing something obvious, but if you have an HTTP-only cookie that holds a sensitive credential, and then pass it to a client component, don't you risk exposing it to client-side code and introducing an XSS vulnerability?

Is the thinking here that unless you're actually rendering the cookie value it shouldn't show up in any part of the browser payload? Edit: looks like props passed to client components can show up in the browser payload, even if they're not used directly:

self.__next_f.push([1,"c:I{\"id\":\"(app-client)/./src/app/org/Name.tsx\",\"chunks\":[\"app/org/page:static/chunks/app/org/page.js\"],\"name\":\"\",\"async\":false}\nb:[[\"$\",\"h1\",null,{\"children\":[\"Hello, \",\"Michael Bluth\",\"!\"]}],[\"$\",\"$Lc\",null,{\"secret\":\"MY_SUPER_SECRET_VALUE\"}]]\n"])

Here's what I did, make a new env variable name it anything you'd like but don't start it with "NEXTPUBLIC" (otherwise that will be bundled in browser) for instance I chose : ENCRYPTION_KEY="12345678"

Now before passing your cookies to a child client component encrypt it using that key in your layout file, in your client component (ApolloWrapper) decrypt it only if typeof window === "undefined" and attach it.

Since no one will have access to your encryption key all your cookies will be encrypted and decrypted safely on the server side, but remember to add some random data into your object before encryption otherwise it might still be easy to crack.

It's not the best we could do but it's probably the best we can do, if NextJS team added some global request object for the server side, we didn't have to deal with all these non-sense.

phryneas commented 7 months ago

That's incredibly hacky, but also a great solution - well done!

iamkd commented 7 months ago

We have encountered the same issue and it is so frustrating. Basically it blocks SSR support for hooks (unless we use a really smart but still hacky solution above). I have created a discussion in the Next.js repo, hopefully it will gain some attention.

phryneas commented 4 months ago

@Stevemoretz I went ahead and published your approach as a npm package that should make this a lot easier to utilize: https://www.npmjs.com/package/ssr-only-secrets

esavitskiy commented 2 months ago
import { setContext } from '@apollo/client/link/context';

const forwardCookieLink = setContext(async () => {
  return import('next/headers').then(({ cookies }) => {
    return {
      headers: {
        cookie: cookies()
          .getAll()
          .map(({ name, value }) => `${name}=${value}`)
          .join(';'),
      },
    };
  });
});

return new NextSSRApolloClient({
    // use the `NextSSRInMemoryCache`, not the normal `InMemoryCache`
    cache: new NextSSRInMemoryCache(),
    link:
      typeof window === "undefined"
        ? ApolloLink.from([
            // in a SSR environment, if you use multipart features like
            // @defer, you need to decide how to handle these.
            // This strips all interfaces with a `@defer` directive from your queries.
            new SSRMultipartLink({
              stripDefer: true,
            }),
            forwardCookieLink,
            httpLink,
          ])
        : httpLink,
  });
phryneas commented 2 months ago

@esavitskiy You cannot use cookies() outside of React Server Components, and NextSSRInMemoryCache is explicitly targeting Client Components (and this whole thread is about SSR of Client Components), so this seems like it wouldn't do what you expect.

esavitskiy commented 2 months ago

@esavitskiy You cannot use cookies() outside of React Server Components, and NextSSRInMemoryCache is explicitly targeting Client Components (and this whole thread is about SSR of Client Components), so this seems like it wouldn't do what you expect.

just try

phryneas commented 2 months ago

image

It seems that this is working in some way, but it is clearly not documented and might break with every update.

phryneas commented 2 months ago

I just verified with the Next.js support forum. This is not a stable feature of Next.js. Please don't do this.

image

esavitskiy commented 2 months ago

This is not a stable feature of Next.js. Please don't do this.

I agree, but it would be nice =)