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

Apollo Server 3 Cookie Issue #5775

Closed
gorkemgunay opened this issue Oct 3, 2021 · 51 comments
Closed

Apollo Server 3 Cookie Issue #5775

gorkemgunay opened this issue Oct 3, 2021 · 51 comments

Comments

@gorkemgunay
Copy link

gorkemgunay commented Oct 3, 2021

I'm trying to do an authentication system with graphql and I want to use cookies (express-session). I'm using apollo-server-express and my goal is to save the user cookie. I checked in postman and its worked but not working in apollo server 3. What do i need to change for work in apollo server 3?

My cookie setting is on.
image

my code is here

import "reflect-metadata";
import { ApolloServer } from "apollo-server-express";
import express from "express";
import { buildSchema } from "type-graphql";
import { createConnection } from "typeorm";
import session from "express-session";
import connectRedis from "connect-redis";
import cors from "cors";

import { RegisterResolver } from "./modules/user/Register";
import { LoginResolver } from "./modules/user/Login";
import { redis } from "./redis";

const startApolloServer = async () => {
  await createConnection();

  const app = express();

  const RedisStore = connectRedis(session);

  app.use(
    cors({
      credentials: true,
      origin: [
        "https://studio.apollographql.com",
        "http://localhost:4000/graphql",
      ],
    })
  );

  app.use(
    session({
      store: new RedisStore({
        client: redis,
      }),
      name: "mycookie",
      secret: "secret key",
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        secure: true,
        sameSite: "none",
        maxAge: 1000 * 60 * 60 * 24 * 365,
      },
    })
  );

  const schema = await buildSchema({
    resolvers: [RegisterResolver, LoginResolver],
  });

  const server = new ApolloServer({
    schema,
    context: ({ req }: any) => ({ req }),
  });

  await server.start();

  server.applyMiddleware({
    app,
    cors: false,
  });

  app.listen(4000, () =>
    console.log("server started on http://localhost:4000/graphql")
  );
};

startApolloServer();

I changed the cors options to this and delete secure:true part for postman. It's work in postman.

 app.use(
    cors({
      credentials: true,
      origin: [
        "http://localhost:4000/graphql",
      ],
    })
  );
@trevor-scheer
Copy link
Member

Hi @gorkemgunay, sorry for the trouble you're having. I just want to make sure - this isn't related to upgrading to ASv3, correct?

Can you please provide me with a reproduction that demonstrates the issue? Ideally a repository that I clone with instructions on how to see the issue you're experiencing.

Thanks!

@gorkemgunay
Copy link
Author

gorkemgunay commented Oct 6, 2021

Hi @trevor-scheer, I don't know the CORS issue related to apollo server v3.

When i use "Postman" i change my cors settings like that

 app.use(
    cors({
      credentials: true,
      origin: [
        // "https://studio.apollographql.com",
        "http://localhost:4000/graphql",
      ],
    })
  );

  app.use(
    session({
      store: new RedisStore({
        client: redis,
      }),
      name: "mycookie",
      secret: "secret key",
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        // secure: true,
        sameSite: "none",
        maxAge: 1000 * 60 * 60 * 24 * 365,
      },
    })
  );

I commented secure:true option and "https://studio.apollographql.com" origin and it's work for postman. Here is the cookie result:

Ekran Resmi 2021-10-06 12 12 28

Ekran Resmi 2021-10-06 11 40 42

Also I tried for apollo-server v3 for these settings. Secure:true and origin is "https://studio.apollographql.com". I commented the "http://localhost:4000/graphql". But this is not work. I didn't understand.

app.use(
    cors({
      credentials: true,
      origin: [
        "https://studio.apollographql.com",
        // "http://localhost:4000/graphql",
      ],
    })
  );

  app.use(
    session({
      store: new RedisStore({
        client: redis,
      }),
      name: "mycookie",
      secret: "secret key",
      resave: false,
      saveUninitialized: false,
      cookie: {
        httpOnly: true,
        secure: true,
        sameSite: "none",
        maxAge: 1000 * 60 * 60 * 24 * 365,
      },
    })
  );

As you can see my cookie doesn't show. I really don't know what the problem is.

Ekran Resmi 2021-10-06 12 13 36

Ekran Resmi 2021-10-06 11 43 17

@cheapsteak
Copy link
Member

cheapsteak commented Oct 6, 2021

Hi @gorkemgunay

Unfortunately browsers require that sameSite: "none" to be accompanied by secure: true otherwise afaik it'll refuse to set the cookie (https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite#samesitenone_requires_secure);

@cheapsteak
Copy link
Member

the problem is also complicated by http://localhost not being able to set a secure cookie, regardless of the setting

expressjs/cookie-session#71 (comment)

[..] when you set secure: true, this module won't even produce a Set-Cookie response header if your connection to Node.js is not over TLS

💭 this is indeed problematic for local development with cookie-based auth

@cheapsteak
Copy link
Member

cheapsteak commented Oct 6, 2021

It looks like at least as of Chrome 89, Chrome will allow http://localhost to set a secure cookie despite being on HTTP rather than HTTPS

Based on how secure: false is generating a cookie for Postman and secure: true isn't, it looks like we're hitting the same issue as the linked problem in cookie-session where the set-cookie header isn't being sent when secure: true and the request is made from a non HTTPS protocol

Verified that looks like what the code is doing here - https://github.com/expressjs/session/blob/a8641429502fcc076c4b2dcbd6b2320891c1650c/index.js#L231-L243 (if issecure is falsey it early returns before calliing setcookie)

In that case, I think these two steps might make this work

  1. setting app.set('trust proxy', true) (highly recommend only setting this for local dev, so perhaps app.set('trust proxy', process.env.NODE_ENV !== 'production') may be better)
  2. when sending the request to log in, attach a x-forwarded-proto header with a value of https so that these lines of code will evaluate to truthy

@cheapsteak
Copy link
Member

looks like there's an open issue on express-session to allow setting secure cookies on localhost here expressjs/session#837

should hopefully be a cleaner solution once that resolves

@gorkemgunay
Copy link
Author

gorkemgunay commented Oct 6, 2021

Hi @cheapsteak, firstly thank you so much for your well explained comments. I added app.set("trust proxy", true); this code to my startApolloServer function and I set the header using studio.apollographql just like that. x-forwarded-proto and https
Ekran Resmi 2021-10-07 00 34 25

This solution is work. Here is the cookie.
Ekran Resmi 2021-10-07 00 34 39

But I have a quesiton. If a deploy this app to https website (production) will it still set my cookie? I think i should delete this part for production app.set("trust proxy", true); . If I delete this part and deploy the app to https website will it still work? What kind of request I need to send via apollo-client?

@cheapsteak
Copy link
Member

Woot, glad that worked!

I would definitely recommend not deploying that line to production

If your production site is served from HTTPS, it shouldn't need it

Can you perhaps add a check for some environment variable so it only runs that line when run locally?

@gorkemgunay
Copy link
Author

Hi @cheapsteak, thank you, I will add the app.set('trust proxy', process.env.NODE_ENV !== 'production')this part in m code.

@glasser
Copy link
Member

glasser commented Oct 7, 2021

Sounds like you've figured this out! I'm going to close the issue.

@jason-liu22
Copy link

I had the same issue, and I followed the steps mentioned above. and then It works.
By the way, Is there a method to make it work for both Postman and Apollo Studio?

@ThalesM1
Copy link

I did the following, but it is not working anymore, does anyone have a clue where to place app.set in the code above?

@jason-liu22
Copy link

2022-03-15_23-33

@ThalesM1
Copy link

ThalesM1 commented Mar 15, 2022

that's exactly where I placed the code @pyDjangoDev

@jason-liu22
Copy link

that's exactly where I placed the code @pyDjangoDev

So it's not working?

@ThalesM1
Copy link

image

@ThalesM1
Copy link

image

I've set to "true", but I'm !prod, of course

@jason-liu22
Copy link

2022-03-15_23-44_1
2022-03-15_23-45

@ThalesM1
Copy link

import "reflect-metadata"
import { MikroORM } from "@mikro-orm/core";
import { prod } from "./constants";
import microConfig from "./mikro-orm.config";
import express from "express"
import {ApolloServer} from 'apollo-server-express'
import { buildSchema } from "type-graphql";
import { HelloResolver } from "./resolvers/hello";
import { PostResolver } from "./resolvers/posts";
import { UserResolver } from "./resolvers/user";

import redis from "redis";
const session = require("express-session")
import connectRedis from 'connect-redis'
import cors from 'cors'

import { MyContext } from "./types";

const main = async () =>{
console.log("dirname", __dirname);
const orm = await MikroORM.init(microConfig);
await orm.getMigrator().up();
const app = express();
const RedisStore = connectRedis(session)
const redisClient = redis.createClient();
app.set('trust proxy', !prod)
app.use(
cors({
credentials: true,
origin: [
"https://studio.apollographql.com",
"http://localhost:4000/graphql",
],

  }),

)
app.use(
session({
name: 'qid',
store: new RedisStore({
client: redisClient,
disableTTL: true,
}),
cookie: {
maxAge: 1000606024365*30, // one year
httpOnly: true,
sameSite: "None", //csrf
secure: prod , //cookie only works in http
},
saveUninitialized: false,
secret: 'udhsuafhuasdhfuasdhfuadsajvqreogqr',
resave: false,
})
)
const generator = orm.getSchemaGenerator();
await generator.updateSchema();

const apolloServer = new ApolloServer({
schema: await buildSchema({
resolvers: [HelloResolver, PostResolver, UserResolver],
validate: false,
}),
context: ({req, res }): MyContext=> ({em: orm.em, req, res})
});

await apolloServer.start();
apolloServer.applyMiddleware({app, cors: false, });

app.listen(4000, () => {
console.log("server started on localhost:4000")
})

// const post = orm.em.create(Post, {title: "fuck"} );
// await orm.em.persistAndFlush(post);

// const posts = await orm.em.find(Post, {});
// console.log(posts)

}

main().catch(err =>{
console.log(err)
});

@jason-liu22
Copy link

please try to set secure to true

@jason-liu22
Copy link

2022-03-16_00-04

@ThalesM1
Copy link

@pyDjangoDev , setted, but not working also. Where can I put the headers in my code?

@jason-liu22
Copy link

@pyDjangoDev , setted, but not working also. Where can I put the headers in my code?

https://github.com/pyDjangoDev/apollo-express-typeorm-blog/blob/main/src/index.ts

@ThalesM1
Copy link

Tried the code above, where the "httpServer" is imported from?
jason-liu22/apollo-express-typeorm-blog@main...ThalesMarzarotto:patch-1

@jason-liu22
Copy link

import http from "http";

@jason-liu22
Copy link

const httpServer = http.createServer(app);

@jason-liu22
Copy link

Tried the code above, where the "httpServer" is imported from? pyDjangoDev/apollo-express-typeorm-blog@main...ThalesMarzarotto:patch-1

But it's not related to your problem.

@ThalesM1
Copy link

Ok, I open the graphql, make my login, but the cookie is not stored, changed the request.credentials to "include"

@ThalesM1
Copy link

image

@ThalesM1
Copy link

ok, got it! Thanks for the help @pyDjangoDev. Here is what we did:

  1. Open a Graphql without Apollo;
  2. Changed request.credential to "include";
  3. Set sameSite to "lax";
  4. Set secure to false;

@jason-liu22
Copy link

ok, got it! Thanks for the help @pyDjangoDev. Here is what we did:

  1. Open a Graphql without Apollo;
  2. Changed request.credential to "include";
  3. Set sameSite to "lax";
  4. Set secure to false;

Anyway, but it has to work with Apollo Studio.
You can try to run my code from the link above.

@ThalesM1
Copy link

@ThalesM1
Copy link

ok, got it! Thanks for the help @pyDjangoDev. Here is what we did:

  1. Open a Graphql without Apollo;
  2. Changed request.credential to "include";
  3. Set sameSite to "lax";
  4. Set secure to false;

Anyway, but it has to work with Apollo Studio. You can try to run my code from the link above.

Yeah, but I don't know where to place the header part of the code, the "x-forwarded-proto"

@ThalesM1
Copy link

ThalesM1 commented Mar 15, 2022

Made your changes, restarted Apollo, not working

@jason-liu22
Copy link

Made your changes, restarted Apollo, not working

This issue is already closed.
If you are interested, please let's collaborate with me on my project.
I am always open to your feedback on my project.

@ThalesM1
Copy link

ThalesM1 commented Mar 15, 2022

I really don't understand, not working for me yet with Apollo, but I will post here if I can make It work. Thank you for your support! I would enjoy to collaborate with you

@jason-liu22
Copy link

I really don't understand, not working for me yet with Apollo, but I will post here if I can make It work. Thank you for your support! I would enjoy to collaborate with you

You are welcome.

@kalanjiyaVishnu
Copy link

kalanjiyaVishnu commented Mar 27, 2022

hello,i had randomly changed many of the settings along the way, Finally I made it worked in the my windows, for some reason I had to switch it over to Linuz and founded myself doing the same thing like changing randomly so i gooogled like for a week now and i guess founded a solution, i don't know you guys solved id or not.
But i wanted to share my pov

first of all i tried with the play ground as you know the studio is new and all

--> i followed the above code,
Open a Graphql without studio;
Changed request.credential to "include";
Set sameSite to "lax";
Set secure to false;
it worked but changing to studio not worked
the reason i believe is that
1.The coookies must be transferred to the studio before the introspection for some reason
it is https so we hav to set x-forwaded proto to https so to receive from our server
2. Because the cookies not set to our server url itself so we made sameSite to "none"

so sol is

const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [userResolver, PostResolver],
      validate: false,
    }),
    context: ({ req, res }: MyContext) => ({ req, res }),
    introspection: true,
    // plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
  })

with introspection on, the studio is live but cookies not being set

app.use(
    session({
      name: "my cookie",
      store: new RedisStore({ client: redisClient, disableTouch: true }),
      cookie: {
        maxAge: 1000 * 60 * 60 * 24 * 365 * 10, //10 years
        httpOnly: true,
        secure: true,
        sameSite: "none",
      },
      saveUninitialized: false,
      secret: process.env.SESSION_SECRET as string,
      resave: false,
    })
  )

this also dosn't set the cookie

but those settings are correct i guess for the production and also for the dev

so i made the cors on the apollo server false and along with the default cors option included the url in the origin

app.set("trust proxy", !_prod_)
  app.use(
    cors({
      credentials: true,
      origin: ["https://studio.apollographql.com", "http://localhost:3000"],
    })
  )

  await apolloServer.start()

  apolloServer.applyMiddleware({
    app,
    cors: false,
    // path: "/",
  })

here is the full code.. if anything wrong with what im saying --> pls correct it. im just a newbie.

import "reflect-metadata"
import Express from "express"
import { ApolloServer } from "apollo-server-express"
import connectRedis from "connect-redis"
import cors from "cors"
import { config } from "dotenv"
import session from "express-session"
import { buildSchema } from "type-graphql"
import { createConnection } from "typeorm"
import { _prod_ } from "./constants/globals"
import { redisClient } from "./redis"
import { PostResolver } from "./resolver/PostResolver"
import { userResolver } from "./resolver/userResolver"
import MyContext from "./types/Context"
import { ApolloServerPluginLandingPageGraphQLPlayground } from "apollo-server-core"
const main = async () => {
  config()

  await createConnection()

  const app = Express()

  const RedisStore = connectRedis(session)

  app.use(
    session({
      name: "my cookie",
      store: new RedisStore({ client: redisClient, disableTouch: true }),
      cookie: {
        maxAge: 1000 * 60 * 60 * 24 * 365 * 10, //10 years
        httpOnly: true,
        secure: true,
        sameSite: "none",
      },
      saveUninitialized: false,
      secret: process.env.SESSION_SECRET as string,
      resave: false,
    })
  )
  const apolloServer = new ApolloServer({
    schema: await buildSchema({
      resolvers: [userResolver, PostResolver],
      validate: false,
    }),
    context: ({ req, res }: MyContext) => ({ req, res }),
    // introspection: true,
    // plugins: [ApolloServerPluginLandingPageGraphQLPlayground()],
  })

  // app.use(function (req, res, next) {
    //   res.header("Access-Control-Allow-Origin", "*")
  //   res.header(
  //     "Access-Control-Allow-Headers",
  //     "Origin, X-Requested-With, Content-Type, Accept"
  //   )
  //   next()
  // })
  app.set("trust proxy", !_prod_)
  app.use(
    cors({
      credentials: true,
      origin: ["https://studio.apollographql.com", "http://localhost:3000"],
    })
  )

  await apolloServer.start()

  apolloServer.applyMiddleware({
    app,
    cors: false,
    // path: "/",
  })

  app.get("/bob", (req, res) => {
    console.log(req.session)

    res.send("bob")
  })

  app.listen(5000, () =>
    console.log(
      `🚀 Server ready at http://localhost:5000${apolloServer.graphqlPath}`
    )
  )
}

main()

@ThalesM1
Copy link

ThalesM1 commented Mar 27, 2022

Cors = False, you mean credentials = false? I do not see that change in the code above

@kalanjiyaVishnu
Copy link

apolloServer.applyMiddleware({ app, cors: false, // path: "/", })
you mean in this line? yes i set the cors to false for this route but applied cors to all routes using middleware

@Axedyson
Copy link

Axedyson commented Aug 22, 2022

I think that the new option to embed the landing page solves many of these problems:

https://www.apollographql.com/docs/studio/explorer/embed-explorer/#embedding-on-the-apollo-server-landing-page
https://www.apollographql.com/docs/apollo-server/api/plugin/landing-pages/#embed
c838e3e

@hitchcliff
Copy link

hitchcliff commented Dec 5, 2022

Try passing the code below in new ApolloServer() method

should be at plugins

 // Apollo Recommended Plugin
  let plugins: any = [];
  if (process.env.NODE_ENV === "production") {
    plugins = [
      ApolloServerPluginLandingPageProductionDefault({
        embed: true,
        graphRef: "myGraph@prod",
        includeCookies: true,
      }),
    ];
  } else {
    plugins = [
      ApolloServerPluginLandingPageLocalDefault({
        embed: true,
        includeCookies: true, //dont forget this
      }),
    ];
  }

@JkNPatel
Copy link

JkNPatel commented Mar 2, 2023

No need to set trust proxy & cors

Solution as per Apollo GraphQL Docs
set cookie setting to

    cookie: {
            maxAge: 1000 * 60 * 60 * 24
            httpOnly: true,
            sameSite: "lax",
            secure: __prod__ 
      },

Includes plugins in ApolloServer contructor // make sure to enable include cookies

    let plugins = [];
    if (__prod__) {
         plugins = [ApolloServerPluginLandingPageProductionDefault({ embed: true, graphRef: 'myGraph@prod', includeCookies: true })]
    } else {
         plugins = [ApolloServerPluginLandingPageLocalDefault({ embed: true, includeCookies: true })]
    }
 
 const apolloServer = new ApolloServer({
        schema: await buildSchema({
            resolvers: [UserResolver],
            validate: false,
        }),
        plugins: [
            ...plugins,
            ApolloServerPluginDrainHttpServer({ httpServer }) // used with expressMiddleware to gracefully shutdown server
        ],
    });   

@DevMobolaji
Copy link

ok, got it! Thanks for the help @pyDjangoDev. Here is what we did:

  1. Open a Graphql without Apollo;
  2. Changed request.credential to "include";
  3. Set sameSite to "lax";
  4. Set secure to false;

Can you show us the code please the request.credentials is it from the back end or frontend

@jyoVerma15
Copy link

I really don't understand, not working for me yet with Apollo, but I will post here if I can make It work. Thank you for your support! I would enjoy to collaborate with you

Facing the same issue , studio is live but still can't find the cookie in request headers. By any chance did you get any solution in this

@trevor-scheer
Copy link
Member

@jyoVerma15 I'd be happy to help you if you provide some more details. Can you share a reproduction?

@jyoVerma15
Copy link

jyoVerma15 commented Apr 19, 2023

@jyoVerma15 I'd be happy to help you if you provide some more details. Can you share a reproduction?

I am trying to access the cookie in the context function as request headers. We are using the apollo-server package and added the cors settings and enabled cookies option in explorer and added sameSite and secure attributes but still I am getting the cookie value as undefined in the request headers in context.

const server = new ApolloServer({
gateway,
cors: {
origin: ['https://studio.apollographql.com', 'http://localhost:3000'],
credentials: true,
},
context: async ({ req }) => {
// Get the Cookie from the
const { cookie } = req.headers;
console.log(cookie); // getting here undefined
},
})
Screenshot 2023-04-19 at 3 19 24 PM

Screenshot 2023-04-19 at 4 04 46 PM

@trevor-scheer
Copy link
Member

@jyoVerma15 It looks like you have everything in place, but there's one gotcha here. Browsers won't let you set the Cookie value this way since it's a forbidden request header. The browser prevents this for security purposes.

However, if your server sends a cookie using a proper Set-Cookie header, your browser will use it in subsequent requests. Here's an example plugin which sets a cookie. I confirmed this works in Explorer.

{
  async requestDidStart() {
    return {
      async willSendResponse({ response }) {
        response.http?.headers.set(
          "Set-Cookie",
          "abcdefg=mycookie; SameSite=None; Secure"
        );
      },
    };
  },
},

The Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers are set by your server via the cors configuration you've set - you don't need to send those from the client (they are response headers).

@jyoVerma15
Copy link

@jyoVerma15 It looks like you have everything in place, but there's one gotcha here. Browsers won't let you set the Cookie value this way since it's a forbidden request header. The browser prevents this for security purposes.

However, if your server sends a cookie using a proper Set-Cookie header, your browser will use it in subsequent requests. Here's an example plugin which sets a cookie. I confirmed this works in Explorer.

{
  async requestDidStart() {
    return {
      async willSendResponse({ response }) {
        response.http?.headers.set(
          "Set-Cookie",
          "abcdefg=mycookie; SameSite=None; Secure"
        );
      },
    };
  },
},

The Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers are set by your server via the cors configuration you've set - you don't need to send those from the client (they are response headers).

Thank a lot @trevor-scheer for the solution , but this didn't work for me, as a workaround I just used Requestly chrome extension( modify headers rule) to pass the cookie from request headers for my testing.

@trevor-scheer
Copy link
Member

@jyoVerma15 I'm glad you got something to work for you. If you want to troubleshoot why my workaround didn't solve the issue for you, I'd need to know what behavior you're seeing now and what changed when you added the plugin. (does your browser see the set-cookie header in the server response)

@github-actions
Copy link
Contributor

This issue has been automatically locked since there has not been any recent activity after it was closed. Please open a new issue for related bugs.
For general questions, we recommend using StackOverflow or our discord server.

@github-actions github-actions bot locked as resolved and limited conversation to collaborators May 25, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests