skip to Main Content

After experimenting with different configurations found on questions similar to this, none of them seem to work. Set-Cookie is being sent by express, however the browser isn’t setting it in Application -> Cookies

This question is for when I am running frontend on localhost:7000 and backend on localhost:4000

Frontend Technologies: vite, react, @tanstack/react-query, graphql-request (fetcher), @graphql-codegen, @graphql-codegen/typescript-react-query (Using this to generate react-query hooks for graphql)

Backend Technologies: @apollo/server, type-graphql, express, express-sessions

Repo for reproducing: https://github.com/Waqas-Abbasi/cookies-not-set-repro

Backend Server:

import 'reflect-metadata';
import 'dotenv/config';
import { expressMiddleware } from '@apollo/server/express4';
import http from 'http';
import { PrismaClient } from '@prisma/client';
import { ApolloServer } from '@apollo/server';
import express from 'express';
import bodyParser from 'body-parser';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import cors, { CorsRequest } from 'cors';
import session from 'express-session';
import buildSchemaFacade from './graphql/buildSchemaFacade';
import { redisStore } from './api/redis';

const SEVEN_DAYS = 1000 * 60 * 60 * 24 * 7;

const {
    NODE_ENV,
    PORT = 4000,
    SESSION_LIFETIME = SEVEN_DAYS,
    SESSION_SECRET
} = process.env;

async function bootstrap() {
    const prisma = new PrismaClient();

    const app = express();
    const httpServer = http.createServer(app);

    const schema = await buildSchemaFacade();

    const server = new ApolloServer({
        schema,
        plugins: [ApolloServerPluginDrainHttpServer({ httpServer })]
    });

    await server.start();

    app.use('/graphql', bodyParser.json());

    app.use(
        '/graphql',
        cors<CorsRequest>({
            origin: 'http://localhost:7000',
            credentials: true
        })
    );

    app.use(
        '/graphql',
        session({
            proxy: true,
            name: 'sessionID',
            cookie: {
                maxAge: SESSION_LIFETIME as number,
                sameSite: 'lax',
                secure: NODE_ENV === 'production',
                httpOnly: true
            },
            resave: false,
            secret: SESSION_SECRET as string,
            saveUninitialized: false,
            store: redisStore
        })
    );

    app.use(
        '/graphql',
        expressMiddleware(server, {
            context: async ({ req, res }) => ({ prisma, req, res })
        })
    );

    await new Promise<void>((resolve) =>
        httpServer.listen({ port: PORT || 4000 }, resolve)
    );

    console.log(`🚀 Server ready at http://localhost:4000/graphql`);
}

bootstrap();

Frontend:

GraphQL Client:

import { GraphQLClient } from 'graphql-request';

export const graphqlClient = new GraphQLClient(import.meta.env.VITE_GRAPHQL_ENDPOINT as string, {
    headers: {
        credentials: 'include',
        mode: 'cors',
    },
});

Login.tsx:

import { useState } from 'react';
import { useNavigate } from 'react-router-dom';

import { useLoginUserMutation } from '@platform/graphql/__generated__/graphql';
import { graphqlClient } from '@platform/graphql/graphqlClient';

export default function Login() {
    const [error, setError] = useState<string | null>(null);

    const { mutate } = useLoginUserMutation(graphqlClient);
    const navigate = useNavigate();

    const onSubmit = (event: any) => {
        event.preventDefault();

        const email = (event.target as HTMLFormElement).email.value;
        const password = (event.target as HTMLFormElement).password.value;

        mutate(
            { email, password },
            {
                onSuccess: (data) => {
                    // navigate('/dashboard/orders');
                    console.log(data);
                },
                onError: (error) => {
                    console.error(error);
                    setError('Something went wrong with logging in');
                },
            }
        );
    };

    return (
        <div className="flex h-screen items-center justify-center bg-slate-100">
            <div className="w-[300px] space-y-4  bg-white p-5">
                <h1 className="text-xl font-bold">Login</h1>
                <form onSubmit={onSubmit} className="flex flex-col space-y-4 text-lg">
                    <div className="flex flex-col space-y-2">
                        <label htmlFor="email">Email</label>
                        <input type="email" id="email" />
                    </div>
                    <div className="flex flex-col space-y-2">
                        <label htmlFor="password">Password</label>
                        <input type="password" id="password" />
                    </div>

                    <button type="submit" className="w-fit  bg-black p-2 px-4 text-white">
                        Login
                    </button>
                </form>
                {error && <div className="text-red-500">{error}</div>}
            </div>
        </div>
    );
}

Response when pressing Login Button:

enter image description here
enter image description here
enter image description here
enter image description here

EDIT:

I also tried solution:

https://community.apollographql.com/t/cookie-not-shown-stored-in-the-browser/1901/8

Setting cookie’s secure field to be true and setting sameSite to none, and also passing x-forwarded-proto with value https in the graphQL client.

Still it does not work. On a side note, it is working as expected on Insomnia, just not on any browsers

EDIT 2:
I’ve also tried replacing graphql-request with urql and apollo client, still the same issue. This leads me to think this might be a backend issue with how express session is initialised, and for some reason the browser does not like the Set-Cookie that is sent from the backend

EDIT 3:

./api/redis:

import connectRedis from 'connect-redis';
import session from 'express-session';
import RedisClient from 'ioredis';

const RedisStore = connectRedis(session);
const redisClient = new RedisClient();

export const redisStore = new RedisStore({ client: redisClient });

export default redisClient;

Edit 4:

Repo for reproducing: https://github.com/Waqas-Abbasi/cookies-not-set-repro

3

Answers


  1. I’m not totally certain, but a few things :

    First, the set-cookie doesn’t show a domain in your cookie which should fall back to the request host, however this might depend on your browser

    Second, you set a path to "/" according to the MDN doc this will not match with your "/graphql" path.

    Finally, your screens look like Firefox, but you say you search your cookies in "Application -> Cookies" and in modern Firefox it should be in "Storage -> Cookies"

    Login or Signup to reply.
  2. While the accepted answer is correct, it did not work for me locally. The solution for me to ensure safe passage of sessionID between GraphQL and the client was this client-side connector:

    import { GraphQLClient } from 'graphql-request';
    
    export const graphqlClient = new GraphQLClient('http://localhost:4000/graphql', {
        headers: {
            'Content-Type': 'application/json',
        },
    });
    

    I’m using http://localhost:7000 and http://localhost:4000/graphql, clearing cookies on both frontend and backend browser tabs, force-refreshing :4000 to generate session, then force-refreshing :7000 to receive the sessionID from GraphQL.

    **Please keep in mind:

    There are other reasons why the Express session would not be passed on from GraphQL to the client and subsequently not stored in browser cookie storage.

    In order for the Express sessionID to propagate from the backend to the frontend, you need to be using the same host. So both sides need to run on localhost, or both on 127.0.0.1.

    These work:

    http://localhost:4000/graphql
    http://localhost:7000/
    
    http://127.0.0.1:4000/graphql
    http://127.0.0.1:7000/
    

    These do not work:

    http://localhost:4000/graphql
    http://127.0.0.1:7000/
    
    http://127.0.0.1:4000/graphql
    http://localhost:7000/
    
    Login or Signup to reply.
  3. In the GraphQL client definition, replace

    headers: {
      credentials: 'include',
      mode: 'cors',
    }
    

    with

    credentials: 'include',
    mode: 'cors'
    

    because these are not headers, they are parameters of the request.

    Without the credentials: 'include' parameter in the request, the fetch method will neither send nor receive any cross-origin cookies, where a difference in the port (localhost:7000 vs. localhost:4000) already makes a request cross-origin. That’s why I believe this to be the solution.

    Wesley LeMahieu’s answer handles the question when a cookie counts as same-site, where the port does not matter when obtaining a site. localhost and 127.0.0.1 are not same-site.

    Cookies with SameSite: Lax are sent or received by a fetch request only if this is same-site. In the question, the fetch request is same-site, but cross-origin, and therefore requires the credentials: 'include' option.

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