Skip to content

Commit e393e6b

Browse files
feat(server): wire AuthCookiePlugin and CSRF into middleware chain
- Add AuthCookiePlugin to graphile preset plugins array - Remove Express middleware approach (doesn't work with grafserv) - Add CSRF middleware after authenticate, before graphile - Update server.ts middleware order Middleware chain: cors → api → authenticate → captcha → csrf → graphile (with AuthCookiePlugin) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d133c29 commit e393e6b

2 files changed

Lines changed: 36 additions & 1 deletion

File tree

graphql/server/src/middleware/graphile.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { isGraphqlObservabilityEnabled } from '../diagnostics/observability';
1414
import { HandlerCreationError } from '../errors/api-errors';
1515
import { observeGraphileBuild } from './observability/graphile-build-stats';
1616
import type { DatabaseSettings } from '../types';
17+
import { AuthCookiePlugin } from '../plugins/auth-cookie-plugin';
1718

1819
const maskErrorLog = new Logger('graphile:maskError');
1920

@@ -210,6 +211,7 @@ const buildPreset = (
210211
): GraphileConfig.Preset => {
211212
return {
212213
extends: [createConstructivePreset(databaseSettings)],
214+
plugins: [AuthCookiePlugin],
213215
pgServices: [
214216
makePgService({
215217
pool,

graphql/server/src/server.ts

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1+
import { createCsrfMiddleware } from '@constructive-io/csrf';
12
import { getEnvOptions } from '@constructive-io/graphql-env';
23
import type { ConstructiveOptions } from '@constructive-io/graphql-types';
34
import { Logger } from '@pgpmjs/logger';
45
import { healthz, poweredBy, svcCache, trustProxy } from '@pgpmjs/server-utils';
56
import { PgpmOptions } from '@pgpmjs/types';
67
import { middleware as parseDomains } from '@constructive-io/url-domains';
7-
import express, { Express, RequestHandler } from 'express';
8+
import express, { Express, NextFunction, Request, RequestHandler, Response } from 'express';
89
import type { Server as HttpServer } from 'http';
910
import graphqlUpload from 'graphql-upload';
1011
import { Pool, PoolClient } from 'pg';
@@ -32,7 +33,9 @@ import { createDebugDatabaseMiddleware } from './middleware/observability/debug-
3233
import { debugMemory } from './middleware/observability/debug-memory';
3334
import { localObservabilityOnly } from './middleware/observability/guard';
3435
import { createRequestLogger } from './middleware/observability/request-logger';
36+
// Auth cookie handling is done via AuthCookiePlugin in grafserv
3537
import { createCaptchaMiddleware } from './middleware/captcha';
38+
import { parseCookieValue, SESSION_COOKIE_NAME } from './middleware/cookie';
3639
import { createUploadAuthenticateMiddleware, uploadRoute } from './middleware/upload';
3740
import { startDebugSampler } from './diagnostics/debug-sampler';
3841

@@ -160,6 +163,36 @@ class Server {
160163
app.post('/upload', uploadAuthenticate, ...uploadRoute);
161164
app.use(authenticate);
162165
app.use(createCaptchaMiddleware());
166+
167+
// CSRF protection for cookie-authenticated requests
168+
// Skip CSRF for Bearer token auth (not vulnerable to CSRF) and anonymous requests
169+
const csrf = createCsrfMiddleware({
170+
cookieOptions: {
171+
httpOnly: false, // SPA clients need to read this via document.cookie
172+
secure: process.env.NODE_ENV === 'production',
173+
sameSite: 'lax',
174+
},
175+
});
176+
const csrfProtect: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
177+
// Skip CSRF for Bearer token auth
178+
const auth = req.headers.authorization;
179+
if (auth?.toLowerCase().startsWith('bearer ')) {
180+
return next();
181+
}
182+
// Skip if no session cookie (anonymous requests)
183+
const sessionCookie = parseCookieValue(req, SESSION_COOKIE_NAME);
184+
if (!sessionCookie) {
185+
return next();
186+
}
187+
// Apply CSRF protection for cookie-authenticated requests
188+
csrf.protect(req as any, res as any, next);
189+
};
190+
const csrfSetToken: RequestHandler = (req: Request, res: Response, next: NextFunction) => {
191+
csrf.setToken(req as any, res as any, next);
192+
};
193+
app.use(csrfSetToken); // Set CSRF token cookie on all requests
194+
app.use('/graphql', csrfProtect); // Enforce CSRF on GraphQL mutations
195+
163196
app.use(graphile(effectiveOpts));
164197
app.use(flush);
165198

0 commit comments

Comments
 (0)