Skip to content

Commit b1190bc

Browse files
fix(auth-cookie): parse grafserv body and set cookies correctly
- Add cookie-parser middleware to support CSRF double-submit pattern - Parse GraphQL body from grafserv's getBody() buffer in AuthCookiePlugin - Set cookies directly on Express response to ensure proper HTTP headers - Fix NaN maxAge by handling unparseable authSettings values The AuthCookiePlugin now correctly intercepts auth mutations and sets session cookies via the Express response, ensuring multiple Set-Cookie headers are sent separately as required by HTTP spec. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b1a0c73 commit b1190bc

5 files changed

Lines changed: 124 additions & 24 deletions

File tree

graphql/server/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,12 +80,14 @@
8080
},
8181
"devDependencies": {
8282
"@aws-sdk/client-s3": "^3.1009.0",
83+
"@types/cookie-parser": "^1.4.10",
8384
"@types/cors": "^2.8.17",
8485
"@types/express": "^5.0.6",
8586
"@types/graphql-upload": "^8.0.12",
8687
"@types/multer": "^2.1.0",
8788
"@types/pg": "^8.18.0",
8889
"@types/request-ip": "^0.0.41",
90+
"cookie-parser": "^1.4.7",
8991
"graphile-test": "workspace:*",
9092
"makage": "^0.3.0",
9193
"nodemon": "^3.1.14",

graphql/server/src/middleware/cookie.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,15 @@ export const getSessionCookieConfig = (
2222
authSettings?: AuthSettings,
2323
rememberMe = false
2424
): CookieConfig => {
25-
const maxAge = rememberMe && authSettings?.rememberMeDuration
26-
? parseInt(authSettings.rememberMeDuration, 10)
27-
: authSettings?.cookieMaxAge
28-
? parseInt(authSettings.cookieMaxAge, 10)
29-
: 86400; // 24 hours default
25+
const DEFAULT_MAX_AGE = 86400; // 24 hours
26+
let maxAge = DEFAULT_MAX_AGE;
27+
if (rememberMe && authSettings?.rememberMeDuration) {
28+
const parsed = parseInt(authSettings.rememberMeDuration, 10);
29+
if (!isNaN(parsed)) maxAge = parsed;
30+
} else if (authSettings?.cookieMaxAge) {
31+
const parsed = parseInt(authSettings.cookieMaxAge, 10);
32+
if (!isNaN(parsed)) maxAge = parsed;
33+
}
3034

3135
return {
3236
secure: authSettings?.cookieSecure ?? process.env.NODE_ENV === 'production',

graphql/server/src/plugins/auth-cookie-plugin.ts

Lines changed: 41 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,20 @@ export const AuthCookiePlugin: GraphileConfig.Plugin = {
220220
}
221221

222222
// Get request body for mutation detection
223-
const body = req.body as GraphQLRequestBody;
223+
// grafserv provides getBody() which returns { type: 'buffer', buffer: Buffer }
224+
let body: GraphQLRequestBody | undefined;
225+
if (typeof event.requestDigest.getBody === 'function') {
226+
try {
227+
const rawBody = await event.requestDigest.getBody() as { type?: string; buffer?: Buffer };
228+
if (rawBody?.type === 'buffer' && rawBody.buffer) {
229+
const jsonStr = rawBody.buffer.toString('utf8');
230+
body = JSON.parse(jsonStr) as GraphQLRequestBody;
231+
}
232+
} catch (e) {
233+
log.debug('[auth-cookie] Failed to parse body from requestDigest');
234+
}
235+
}
236+
body = body || (req.body as GraphQLRequestBody);
224237
if (!body?.query) {
225238
return result;
226239
}
@@ -283,24 +296,38 @@ export const AuthCookiePlugin: GraphileConfig.Plugin = {
283296
}
284297
}
285298

286-
// Return modified result with Set-Cookie headers
299+
// Set cookies directly on Express response and return modified headers
287300
if (cookiesToSet.length > 0) {
288-
const existingSetCookie = bufferResult.headers['set-cookie'];
289-
let newSetCookie: string;
290-
291-
if (existingSetCookie) {
292-
// Append to existing Set-Cookie
293-
newSetCookie = `${existingSetCookie}, ${cookiesToSet.join(', ')}`;
294-
} else {
295-
newSetCookie = cookiesToSet.join(', ');
301+
const res = (event.requestDigest.requestContext as { expressv4?: { res?: { setHeader: (name: string, value: string[]) => void; getHeader: (name: string) => string | string[] | undefined } } })?.expressv4?.res;
302+
303+
if (res?.setHeader) {
304+
// Get existing Set-Cookie headers from Express response
305+
const existingCookies = res.getHeader('Set-Cookie');
306+
const allCookies: string[] = [];
307+
308+
if (existingCookies) {
309+
if (Array.isArray(existingCookies)) {
310+
allCookies.push(...existingCookies);
311+
} else {
312+
allCookies.push(existingCookies);
313+
}
314+
}
315+
allCookies.push(...cookiesToSet);
316+
317+
// Set as array to get multiple Set-Cookie headers
318+
res.setHeader('Set-Cookie', allCookies);
296319
}
297320

321+
// Also update the BufferResult headers for grafserv to pass through
322+
const existingBufferCookie = bufferResult.headers['set-cookie'];
323+
const updatedHeaders = { ...bufferResult.headers };
324+
325+
// Remove set-cookie from grafserv headers since we set it on Express
326+
delete updatedHeaders['set-cookie'];
327+
298328
return {
299329
...bufferResult,
300-
headers: {
301-
...bufferResult.headers,
302-
'set-cookie': newSetCookie,
303-
},
330+
headers: updatedHeaders,
304331
};
305332
}
306333
} catch (err) {

graphql/server/src/server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Logger } from '@pgpmjs/logger';
55
import { healthz, poweredBy, svcCache, trustProxy } from '@pgpmjs/server-utils';
66
import { PgpmOptions } from '@pgpmjs/types';
77
import { middleware as parseDomains } from '@constructive-io/url-domains';
8+
import cookieParser from 'cookie-parser';
89
import express, { Express, NextFunction, Request, RequestHandler, Response } from 'express';
910
import type { Server as HttpServer } from 'http';
1011
import graphqlUpload from 'graphql-upload';
@@ -148,6 +149,7 @@ class Server {
148149
}
149150

150151
app.use(poweredBy('constructive'));
152+
app.use(cookieParser());
151153
app.use(cors(fallbackOrigin));
152154
app.use('/graphql', graphqlUpload.graphqlUploadExpress({
153155
maxFileSize: 10 * 1024 * 1024, // 10 MB

0 commit comments

Comments
 (0)