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
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.