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:
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
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"
While the accepted answer is correct, it did not work for me locally. The solution for me to ensure safe passage of
sessionID
betweenGraphQL
and the client was this client-side connector:I’m using
http://localhost:7000
andhttp://localhost:4000/graphql
, clearing cookies on both frontend and backend browser tabs, force-refreshing :4000 to generate session, then force-refreshing :7000 to receive thesessionID
fromGraphQL
.**Please keep in mind:
There are other reasons why the
Express
session would not be passed on fromGraphQL
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 onlocalhost
, or both on127.0.0.1
.These work:
These do not work:
In the GraphQL client definition, replace
with
because these are not headers, they are parameters of the request.
Without the
credentials: 'include'
parameter in the request, thefetch
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
and127.0.0.1
are not same-site.Cookies with
SameSite: Lax
are sent or received by afetch
request only if this is same-site. In the question, thefetch
request is same-site, but cross-origin, and therefore requires thecredentials: 'include'
option.