Apollo-client: [NextJS]: HttpOnly Cookie

Created on 24 Jul 2019  路  2Comments  路  Source: apollographql/apollo-client

Hi, I've been trying to send httpOnly cookie to the Apollo Server using Apollo Client / NextJS.
The GraphQL Playground is properly setting the httpOnly Cookie when I test queries.

Intended outcome:
Apollo Server gets the cookie & get the auth user.

import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import fetch from 'isomorphic-unfetch';
import getConfig from 'next/config';
import { createHttpLink } from 'apollo-link-http';
const { publicRuntimeConfig: config } = getConfig();

let apolloClient: any = null;

if (!process.browser) {
  global.fetch = fetch;
}

function create(initialState: any) {
  return new ApolloClient({
    connectToDevTools: process.browser,
    ssrMode: !process.browser,
    link: createHttpLink({
      credentials: 'include',
      uri: config.API_ENDPOINT
    }),
    cache: new InMemoryCache().restore(initialState || {})
  });
}

export default function initApollo(initialState?: any) {
  if (!process.browser) {
    return create(initialState);
  }

  if (!apolloClient) {
    apolloClient = create(initialState);
  }

  return apolloClient;
}

Actual outcome:
Well the cookie is never inside the req object of context.
Sometimes it works but I really don't know why, like when I save a file and the HMR is starting...

How to reproduce the issue:

const IN_PROD = process.env.NODE_ENV === 'production';
const { APOLLO_PORT, JWT_SECRET } = process.env as any;

import express from 'express';
import { ApolloServer } from 'apollo-server-express';
import { makeSchema } from 'nexus';
import cookieParser from 'cookie-parser';
import jwt from 'jsonwebtoken';
import * as types from './types';

import { default as dataSources } from './datasources';

const app = express();
app.use(cookieParser());

const server = new ApolloServer({
  dataSources: () => {
    return dataSources;
  },
  schema: makeSchema({
    types,
    outputs: false
  }),
  playground: !IN_PROD,
  context: async ({ req, res }) => {
    const { token } = req.cookies;
    const user = await (token ? jwt.verify(token, JWT_SECRET) : null);
    return {
      res,
      currentUser: user,
    };
  }
});

server.applyMiddleware({
  app,
  path: '/',
  cors: {
    origin: [
      ...
    ],
    credentials: true
  }
});

app.listen({ port: APOLLO_PORT }, () =>
  console.log(
    `馃殌 Server ready at http://localhost:${APOLLO_PORT}${server.graphqlPath}`
  )
);

Versions

System:
    OS: macOS 10.14.5
  Binaries:
    Node: 10.15.0 - ~/.nvm/versions/node/v10.15.0/bin/node
    Yarn: 1.15.2 - /usr/local/bin/yarn
    npm: 6.4.1 - ~/.nvm/versions/node/v10.15.0/bin/npm
  Browsers:
    Chrome: 75.0.3770.142
    Firefox: 67.0.4
    Safari: 12.1.1

    "apollo-cache-inmemory": "^1.6.2",
    "apollo-client": "^2.6.3",
    "apollo-link": "^1.2.12",
    "apollo-link-context": "^1.0.18",
    "apollo-link-error": "^1.1.11",
    "apollo-link-http": "^1.5.15",

Most helpful comment

Well, for anyone having this kind of problem, I ended up using next-with-apollo for the frontend.

// _app.tsx
import * as React from 'react';
import App, { Container } from 'next/app';
import { ThemeProvider } from 'emotion-theming';
import { withApollo, loadFonts } from '@lib';
import { Filters, theme } from '@ui';
import { ApolloProvider } from '@apollo/react-hooks';

class MyApp extends App {
  static async getInitialProps({ Component, ctx }: any) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  componentDidMount() {
    loadFonts();
  }

  render() {
    const { Component, pageProps, apollo } = this.props as any;
    return (
      <Container>
        <ThemeProvider theme={theme}>
          <ApolloProvider client={apollo}>
            <Component {...pageProps} />
          </ApolloProvider>
        </ThemeProvider>
        <Filters />
      </Container>
    );
  }
}

export default withApollo(MyApp);
// withApollo.ts
import withApollo from 'next-with-apollo';
import { ApolloClient } from 'apollo-client';
import fetch from 'isomorphic-unfetch';
import getConfig from 'next/config';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
const { publicRuntimeConfig: config } = getConfig();

export default withApollo(({ initialState, headers }: any) => {
  const isBrowser = typeof window !== 'undefined';
  return new ApolloClient({
    connectToDevTools: isBrowser,
    ssrMode: !isBrowser,
    link: createHttpLink({
      uri: config.API_ENDPOINT,
      credentials: 'include',
      ...(!isBrowser && { fetch }),
      headers
    }),
    cache: new InMemoryCache().restore(initialState || {})
  });
});
    ...
    "@apollo/react-hooks": "^0.1.0-beta.11",
    "apollo-cache-inmemory": "^1.6.2",
    "apollo-client": "^2.6.3",
    "apollo-link-http": "^1.5.15",
    "next": "^9.0.2",
    "next-with-apollo": "^4.0.0",
    ...

All 2 comments

Well, for anyone having this kind of problem, I ended up using next-with-apollo for the frontend.

// _app.tsx
import * as React from 'react';
import App, { Container } from 'next/app';
import { ThemeProvider } from 'emotion-theming';
import { withApollo, loadFonts } from '@lib';
import { Filters, theme } from '@ui';
import { ApolloProvider } from '@apollo/react-hooks';

class MyApp extends App {
  static async getInitialProps({ Component, ctx }: any) {
    let pageProps = {};

    if (Component.getInitialProps) {
      pageProps = await Component.getInitialProps(ctx);
    }

    return { pageProps };
  }

  componentDidMount() {
    loadFonts();
  }

  render() {
    const { Component, pageProps, apollo } = this.props as any;
    return (
      <Container>
        <ThemeProvider theme={theme}>
          <ApolloProvider client={apollo}>
            <Component {...pageProps} />
          </ApolloProvider>
        </ThemeProvider>
        <Filters />
      </Container>
    );
  }
}

export default withApollo(MyApp);
// withApollo.ts
import withApollo from 'next-with-apollo';
import { ApolloClient } from 'apollo-client';
import fetch from 'isomorphic-unfetch';
import getConfig from 'next/config';
import { createHttpLink } from 'apollo-link-http';
import { InMemoryCache } from 'apollo-cache-inmemory';
const { publicRuntimeConfig: config } = getConfig();

export default withApollo(({ initialState, headers }: any) => {
  const isBrowser = typeof window !== 'undefined';
  return new ApolloClient({
    connectToDevTools: isBrowser,
    ssrMode: !isBrowser,
    link: createHttpLink({
      uri: config.API_ENDPOINT,
      credentials: 'include',
      ...(!isBrowser && { fetch }),
      headers
    }),
    cache: new InMemoryCache().restore(initialState || {})
  });
});
    ...
    "@apollo/react-hooks": "^0.1.0-beta.11",
    "apollo-cache-inmemory": "^1.6.2",
    "apollo-client": "^2.6.3",
    "apollo-link-http": "^1.5.15",
    "next": "^9.0.2",
    "next-with-apollo": "^4.0.0",
    ...

I'm using apollo-boost with next-with-apollo and httpOnly works perfectly locally but not in production. Anyone else seen the same?

Was this page helpful?
0 / 5 - 0 ratings