|
| 1 | +import { createCsrfMiddleware } from '@constructive-io/csrf'; |
1 | 2 | import { getEnvOptions } from '@constructive-io/graphql-env'; |
2 | 3 | import type { ConstructiveOptions } from '@constructive-io/graphql-types'; |
3 | 4 | import { Logger } from '@pgpmjs/logger'; |
4 | 5 | import { healthz, poweredBy, svcCache, trustProxy } from '@pgpmjs/server-utils'; |
5 | 6 | import { PgpmOptions } from '@pgpmjs/types'; |
6 | 7 | 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'; |
8 | 9 | import type { Server as HttpServer } from 'http'; |
9 | 10 | import graphqlUpload from 'graphql-upload'; |
10 | 11 | import { Pool, PoolClient } from 'pg'; |
@@ -32,7 +33,9 @@ import { createDebugDatabaseMiddleware } from './middleware/observability/debug- |
32 | 33 | import { debugMemory } from './middleware/observability/debug-memory'; |
33 | 34 | import { localObservabilityOnly } from './middleware/observability/guard'; |
34 | 35 | import { createRequestLogger } from './middleware/observability/request-logger'; |
| 36 | +// Auth cookie handling is done via AuthCookiePlugin in grafserv |
35 | 37 | import { createCaptchaMiddleware } from './middleware/captcha'; |
| 38 | +import { parseCookieValue, SESSION_COOKIE_NAME } from './middleware/cookie'; |
36 | 39 | import { createUploadAuthenticateMiddleware, uploadRoute } from './middleware/upload'; |
37 | 40 | import { startDebugSampler } from './diagnostics/debug-sampler'; |
38 | 41 |
|
@@ -160,6 +163,36 @@ class Server { |
160 | 163 | app.post('/upload', uploadAuthenticate, ...uploadRoute); |
161 | 164 | app.use(authenticate); |
162 | 165 | 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 | + |
163 | 196 | app.use(graphile(effectiveOpts)); |
164 | 197 | app.use(flush); |
165 | 198 |
|
|
0 commit comments