skip to Main Content

I created a simple server with Express Session and Redis. The Redis server is running (I received ‘PONG’ when I typed ‘redis-cli ping’), and I declared the namespace and exported the interface SessionData to allow me to store userID on req.session (using a basic index.d.ts). However, when I make the login request, the variable userID is stored on req.session, but since the cookie is not set to the browser, it is immediately forgotten/erased. It seems like every request produces a new session and the cookie never saves.

App, Redis, and Session Cookie setup:

// ...
const app = express();

const RedisStore = connectRedis(session);
const redis = new Redis();

app.use(
    session({
      name: 'testcookie',
      store: new RedisStore({
        client: redis,
        disableTouch: true,
        port: 6379,
        host: 'localhost',
      }),
      cookie: {
        maxAge: 36000000 * 24 * 365,
        httpOnly: true,
        sameSite: 'lax',
        secure: false,
      },
      saveUninitialized: false,
      secret: 'secret',
      resave: false,
    })
  );
// ...

Login mutation:

@Mutation(() => UserResponse)
  async login(
    @Arg("usernameOrEmail") usernameOrEmail: string,
    @Arg("password") password: string,
    @Ctx() { req }: MyContext
  ): Promise<UserResponse> {
    // gets user via inputs (everything works here)
    // ...

    req.session.userID = user.id;
    // userID is set to be a number, as is user.id
    // logging req.session.userID works perfectly if done right here

    return { user };
  }

Query to check if logged in:

@Query(() => User, { nullable: true })
  async me(@Ctx() { req }: MyContext): Promise<User | undefined> {
    // logging req.session.userID shows undefined

    return (req.session.userID)
      ? await User.findOne({ id: req.session.userID })
      : undefined;
  }

UPDATE (SOLUTION): This was resolved by going into GraphQL’s settings and changing the "request.credentials" property to "include."

2

Answers


  1. I am following the same tutorial Fullstack React GraphQL TypeScript Tutorial in Jun 2022. Since 2021, Apollo’s GraphQL Playground does not exist- in it’s place there is Apollo studio sandbox (https://studio.apollographql.com/sandbox/explorer)

    There is no way I could find to set request.credentials to include in the new apollo studio.

    After following these threads:

    https://community.apollographql.com/t/allow-cookies-to-be-sent-alongside-request/920

    and

    https://github.com/apollographql/apollo-server/issues/5775

    I came to a not-so-great solution, but that works for what I need.

    Essentially, in my setup it seems like the session.cookie parameters ‘sameSite’ and ‘secure’ need to be different values depending on if you want your front end to add the cookie vs the apollo studio to add the cookie. This is NOT IDEAL – but I could not find any other combinations of parameters that worked for both. So far I’ve only found mutually exclusive settings.

    On my server’s index.ts

    I use this session setup when I want to set cookie from front-end localhost:3000

    
      app.use(
        session({
          name: COOKIE_NAME,
          store: new RedisStore({ client: redis, disableTouch: true }),
          saveUninitialized: false,
          cookie: {
            maxAge: 1000 * 60 * 60 * 24 * 12, //2 weeks
            httpOnly: true,
            sameSite: "lax", // sets cookie from frontend localhost:3000
            secure: false, // sets cookie from frontend localhost:3000
          },
          secret: "shhhhdonttell",
          resave: false,
        })
      );
    
    
    
    

    I actually change the session setup if I want to set the cookie and session userId from the apollo studio

    I use this session setup when I want to set cookie from backend-end localhost:4000/graphql

    
      app.use(
        session({
          name: COOKIE_NAME,
          store: new RedisStore({ client: redis, disableTouch: true }),
          saveUninitialized: false,
          cookie: {
            maxAge: 1000 * 60 * 60 * 24 * 12, //2 weeks
            httpOnly: true,
            sameSite: "none", // sets cookie from apollo studio
            secure: true, // sets cookie from apollo studio
          },
          secret: "shhhhdonttell",
          resave: false,
        })
      );
    
    

    Please comment if you know of a way to use the same settings to allow cookies from both front and backend.

    For those of you who are following the tutorial and want more details, here are the other important parts of the setup.

    This is my entire index.ts file from backend. Note – I am 8.5 hours into the tutorial – so don’t worry if you don’t recognize some parts.

    
    import "reflect-metadata";
    import { COOKIE_NAME, __prod__ } from "./constants";
    import express from "express";
    import { ApolloServer } from "apollo-server-express";
    import { buildSchema } from "type-graphql";
    import { HelloResolver } from "./resolvers/hello";
    import { PostResolver } from "./resolvers/post";
    import { UserResolver } from "./resolvers/user";
    import ioredis from "ioredis";
    import session from "express-session";
    import connectRedis from "connect-redis";
    import { MyContext } from "./types";
    import cors from "cors";
    import { getDataSource } from "./data-source";
    import { Post } from "./entities/Post";
    
    const PORT = 4000;
    
    const main = async () => {
      const dbconn = await getDataSource()
    
      if (typeof dbconn === "boolean") return
    
      console.log('starting migrations')
      dbconn.runMigrations()
      // await Post.delete({})
    
      // const orm = await MikroORM.init(microConfig);
      // orm.getMigrator().up();
      console.log('migrations finished')
    
      const app = express();
    
      const RedisStore = connectRedis(session);
      const redis = new ioredis();
      // const redisClient = new redis({ legacyMode: true });
      redis.connect().catch((err) => `RedisClient Connect error: ${err}`);
    
      !__prod__ && app.set("trust proxy", 1);
    
      app.use(
        cors({
          origin: ["http://localhost:3000", "http://localhost:4000/graphql", "https://studio.apollographql.com",],
          credentials: true,
        })
      );
    
      app.use(
        session({
          name: COOKIE_NAME,
          store: new RedisStore({ client: redis, disableTouch: true }),
          saveUninitialized: false,
          cookie: {
            maxAge: 1000 * 60 * 60 * 24 * 12, //2 weeks
            httpOnly: true,
            sameSite: "lax", 
            secure: __prod__,
          },
          secret: "shhhhdonttell",
          resave: false,
        })
      );
    
      // app.use(
      //   session({
      //     name: COOKIE_NAME,
      //     store: new RedisStore({ client: redis, disableTouch: true }),
      //     saveUninitialized: false,
      //     cookie: {
      //       maxAge: 1000 * 60 * 60 * 24 * 12, //2 weeks
      //       httpOnly: true,
      //       // sameSite: "lax", // front end 
      //       // secure: __prod__, //true in production, is can be false for frontend
      //       sameSite: "none", //csrf //must be nne for apollo sandbox
      //       secure: true, //true in production, false on localhost // must be true for apollo sandbox even in dev
      //     },
      //     secret: "shhhhdonttell",
      //     resave: false,
      //   })
      // );
    
      const apolloServer = new ApolloServer({
        schema: await buildSchema({
          resolvers: [HelloResolver, PostResolver, UserResolver],
          validate: false,
        }),
        context: ({ req, res }): MyContext => ({
          dbconn,
          req,
          res,
          redis,
        }),
      });
    
      await apolloServer.start();
      apolloServer.applyMiddleware({
        app,
        cors: false,
        // cors: {
        //   // origin: "http://localhost:3000",
        //   origin: [
        //     "http://localhost:3000",
        //     "https://studio.apollographql.com",
        //     "http://localhost:4000/graphql",
        //   ],
        //   credentials: true,
        // },
      });
    
      app.listen(PORT, () => {
        console.log(`server started on localhost:${PORT}`);
      });
    };
    
    main().catch((err) => {
      console.error(err);
    });
    
    

    Here is my data-source (the new way to make typeORM connection!)

    import { Post } from "./entities/Post";
    import { Users } from "./entities/Users";
    import { DataSource } from "typeorm";
    import path from "path"
    import "reflect-metadata";
    import { Upvote } from "./entities/Upvote";
    
    export async function getDataSource() {
    
        const typeormDataSource = new DataSource({
            type: "postgres",
            host: "localhost",
            port: 5432,
            username: "nicole",
            password: "yourpassword",
            database: "bendb2", //your dbname
            logging: false,
            synchronize: true,
            entities: [Post, Users, Upvote],//take out entites you have't made yet.
            subscribers: [],
            migrations: [path.join(__dirname, "./migrations/*")],
    
        })
        let datasource = await typeormDataSource.initialize().catch(err => {
            console.error(err)
            console.log("Database connection NOT initialized")
            return false
        })
        return datasource
    }
    
    
    

    in the createUrqlClient.tsx front end file, I have added

    export const createUrqlClient = (ssrExchange: any) => ({
      url: "http://localhost:4000/graphql",
      fetchOptions: {
        credentials: "include" as const, 
      },
      exchanges: [...]
    
    

    Here is a snapshot of the settings needed in apollo studio. To open these settings, click on the settings/gear icon at the top left inside the SANDBOX input.

    apollo studio settings

    make sure to add ‘x-forwarded-proto’ ‘https’ to the Shared Headers.

    Login or Signup to reply.
  2. The answer form @NicoWheat is partial right (I guess, correct me if I am wrong). It worked when I send the request with apollo studio (with sameSite:"none" and secure: true) but regardless what are the options the cookies still has not been set for me when I do the mutation through localhost:3000.

    Edit: I was wrong, after following the option in create urql client in frontend directory, it worked for me, thanks and appreciate it a lot.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search