Skip to content

feat(graphql-server): Cookie Lifecycle & CSRF Enforcement#1041

Open
theothersideofgod wants to merge 9 commits intomainfrom
feat/server-auth-cookie-csrf
Open

feat(graphql-server): Cookie Lifecycle & CSRF Enforcement#1041
theothersideofgod wants to merge 9 commits intomainfrom
feat/server-auth-cookie-csrf

Conversation

@theothersideofgod
Copy link
Copy Markdown
Contributor

@theothersideofgod theothersideofgod commented Apr 30, 2026

Summary

Implements server-side cookie management for authentication flows, addressing #749.

  • Session cookies: Automatically set constructive_session cookie on successful auth mutations (signIn, signUp, SSO, MFA, etc.)
  • Sign out: Clear both session and device token cookies on signOut/revokeSession/revokeAllSessions
  • CSRF protection: Integrated with existing CSRF middleware, preserves CSRF token cookies
  • Multi-tenant support: Works correctly with both main database and tenant databases

Why grafserv Plugin Instead of Express Middleware?

Initially attempted to implement this using Express middleware, but encountered architectural limitations:

Express middleware approach (doesn't work):

// Attempted to intercept res.json() to set cookies
const originalJson = res.json.bind(res);
res.json = (body) => {
  if (body?.data?.signIn?.accessToken) {
    res.cookie('constructive_session', body.data.signIn.accessToken);
  }
  return originalJson(body);
};

The problem: PostGraphile v5 (grafserv) uses streaming responses. The response is serialized to a buffer internally by grafserv and written directly to the stream — it never passes through res.json(). Express middleware cannot intercept this.

grafserv plugin approach (correct solution):

grafserv: {
  middleware: {
    async processRequest(next, event) {
      const result = await next();  // Get complete response
      // Parse buffer, extract accessToken, set cookies
      return { ...result, headers: { 'set-cookie': [...] } };
    }
  }
}

This allows us to modify headers before grafserv sends the response, correctly setting cookies at the right layer.

Key Changes

New Files

  • graphql/server/src/plugins/auth-cookie-plugin.ts - grafserv plugin for cookie lifecycle
  • graphql/server/src/plugins/__tests__/auth-cookie-plugin.test.ts - 25 unit tests

Modified Files

  • graphql/server/src/middleware/graphile.ts - Wire AuthCookiePreset into PostGraphile
  • graphql/server/src/middleware/cookie.ts - Fix rememberMe duration logic
  • graphql/server/src/server.ts - Remove old Express middleware

Removed Files

  • graphql/server/src/middleware/auth-cookie.ts - Old Express middleware (didn't work with grafserv streaming)
  • graphql/server/src/middleware/__tests__/auth-cookie.test.ts - Old tests

Cookie Behavior

Mutation Action
signIn, signUp, signInSso, signUpSso, signInMagicLink, signUpMagicLink, signInEmailOtp, signInSmsOtp, signUpSms, signInOneTimeToken, signInCrossOrigin, completeMfaChallenge, refreshToken Set constructive_session cookie (+ constructive_device_token if returned)
signOut, revokeSession, revokeAllSessions Clear both cookies

Cookie Attributes

  • HttpOnly: true (XSS protection)
  • SameSite: Lax (CSRF protection)
  • Path: /
  • MaxAge: 1 hour default, 30 days with rememberMe: true

Test Plan

  • Unit tests: 45 tests passing (25 plugin + 11 CSRF + 9 cookie)
  • Integration test: Main database (api.localhost:3000)
  • Integration test: Tenant database (auth-xxx.localhost:3000)
  • Verify CSRF token preserved alongside session cookie
  • Verify signOut clears both session and device cookies

Related

Closes #749


🤖 Generated with Claude Code

theothersideofgod and others added 9 commits April 30, 2026 17:13
- Add AuthSettings interface for cookie configuration
- Add cookie.ts with session/device cookie helpers:
  - buildCookieOptions() - builds Express cookie options from settings
  - setSessionCookie() / clearSessionCookie()
  - setDeviceTokenCookie() / clearDeviceTokenCookie()
- Support rememberMe for extended session duration
- Parse PostgreSQL interval format for durations

Refs: constructive-io/constructive-planning#749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Intercept auth mutation responses (signIn, signUp, signOut, etc.)
- Set constructive_session cookie on successful authentication
- Clear session cookie on signOut/revokeSession
- Set constructive_device_token cookie for device tracking
- Support rememberMe from mutation input variables

Refs: constructive-io/constructive-planning#749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add sessions_module query to get auth_settings table location
- Add queryAuthSettings() to fetch settings from tenant DB
- Update toApiStructure() to include authSettings
- Load auth settings in parallel with RLS module

Refs: constructive-io/constructive-planning#749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add cookie-parser and @constructive-io/csrf dependencies
- Add CSRF middleware with double-submit cookie pattern
- Skip CSRF check for Bearer token authentication
- Add auth-cookie middleware to intercept responses
- Add CSRF error codes to SAFE_ERROR_CODES allowlist

Refs: constructive-io/constructive-planning#749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add cookie.test.ts with 9 tests for cookie helpers
- Add auth-cookie.test.ts with 13 tests for middleware
- Test session cookie set/clear on auth mutations
- Test device token cookie handling
- Test rememberMe duration extension

Refs: constructive-io/constructive-planning#749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Test Bearer token authentication skips CSRF
- Test GET/HEAD/OPTIONS requests skip CSRF
- Test anonymous requests (no session cookie) skip CSRF
- Test cookie-authenticated requests require valid CSRF token
- Test CSRF token validation (missing, invalid, valid)
- Test CSRF token from header and body

Refs: constructive-io/constructive-planning#749

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The captcha middleware references this property, so it needs to be
included in the AuthSettings type definition.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
…v plugin

- Replace Express middleware with grafserv processRequest plugin
- Fix cookie handling for grafserv streaming responses (buffer parsing)
- Handle multiple Set-Cookie headers correctly (array format)
- Preserve CSRF cookies when setting session cookies
- Fix rememberMe cookie duration logic
- Remove old middleware and tests (will add plugin tests separately)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Test preset structure and plugin metadata
- Test session cookie on auth mutations (signIn, signUp, etc.)
- Test cookie clearing on sign out mutations
- Test device token cookie handling
- Test CSRF cookie preservation
- Test buffer and JSON response handling
- Test mutation detection regex patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@theothersideofgod theothersideofgod force-pushed the feat/server-auth-cookie-csrf branch from 7263419 to 8b29938 Compare April 30, 2026 09:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant