feat: Tier 0 server auth - cookie lifecycle, CSRF enforcement, device tokens, SSO wiring#1004
Open
pyramation wants to merge 3 commits intomainfrom
Open
feat: Tier 0 server auth - cookie lifecycle, CSRF enforcement, device tokens, SSO wiring#1004pyramation wants to merge 3 commits intomainfrom
pyramation wants to merge 3 commits intomainfrom
Conversation
… tokens, SSO wiring - Add cookie lifecycle middleware (cookie.ts): intercepts auth mutation responses and sets/clears HttpOnly session cookies when enable_cookie_auth is true in app_auth_settings. Handles all sign-in/sign-up/sign-out mutations. - Add CSRF protection middleware (csrf.ts): wires @constructive-io/csrf package into server. Only enforces on cookie-authenticated requests, completely skips Bearer token requests. Controlled by require_csrf_for_auth toggle in app_auth_settings. - Add device token cookie support: on sign-in responses that include a device_id, sets a long-lived (90 day) constructive_device_token cookie for trusted device tracking. - Add SSO/OAuth route middleware (oauth.ts): mounts /auth/:provider and /auth/:provider/callback routes. On successful OAuth callback, calls sign_in_sso() on the tenant DB private schema and optionally sets session cookie when cookie auth is enabled. - Expand AuthSettings interface with enableCookieAuth and requireCsrfForAuth toggles. Update AUTH_SETTINGS_SQL query to fetch these columns. - Set req.tokenSource on authenticated requests so downstream middleware knows whether auth came from bearer header, cookie, or none. - Add cookie-parser, @constructive-io/csrf, @constructive-io/oauth as server dependencies. Backward compatibility: - All features are opt-in via app_auth_settings toggles (default: off) - Bearer token authentication continues to work exactly as before - CSRF only enforces on cookie-authenticated requests - No changes to existing GraphQL mutations or API contracts
Contributor
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
…okie injection grafserv (PostGraphile v5) writes responses via res.writeHead() + res.end(buffer), completely bypassing Express's res.json(). The previous res.json monkey-patch would never fire for GraphQL responses. New approach: defer res.writeHead(), intercept res.end() to parse the JSON body, compute Set-Cookie headers, merge them into the deferred headers, then flush writeHead + end together. Also adds serializeCookie/serializeClearCookie helpers since we can't rely on Express's res.cookie() when headers are managed manually.
…NodeEnv() - Add OAuthOptions and CaptchaOptions to ConstructiveOptions type - Add OAUTH_* and RECAPTCHA_SECRET_KEY parsing to getGraphQLEnvVars() - Replace process.env.NODE_ENV with getNodeEnv() in cookie.ts, csrf.ts, oauth.ts, server.ts - Refactor oauth.ts to read providers/baseUrl/redirects from opts.oauth - Refactor captcha.ts to read recaptchaSecretKey from opts.captcha - Pass effectiveOpts to createCaptchaMiddleware() in server.ts - Zero process.env references remain in new middleware files
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the server-side foundation for cookie-based authentication as an opt-in layer alongside existing Bearer token auth. All features are gated behind
app_auth_settingstoggles (enable_cookie_auth,require_csrf_for_auth) and are off by default — zero behavior changes for existing API token flows.New middleware files:
cookie.ts— Intercepts auth mutation responses (signIn,signUp,completeMfaChallenge, etc.) to set/clear anHttpOnlyconstructive_sessioncookie from the returnedaccess_token. Also sets a long-livedconstructive_device_tokencookie when device tracking returns adevice_id. Only active whenenable_cookie_auth = true. Usesres.writeHead()/res.end()interception (notres.json()) because grafserv bypasses Express response helpers — see details below.csrf.ts— Wires the existing@constructive-io/csrfpackage into the middleware chain. CSRF validation is only enforced on cookie-authenticated requests (req.tokenSource === 'cookie'). Bearer token requests skip CSRF entirely since they're not vulnerable to CSRF attacks.oauth.ts— Mounts/auth/:provider,/auth/:provider/callback, and/auth/providersroutes. On successful OAuth callback, callssign_in_sso()on the tenant DB private schema and optionally sets the session cookie.captcha.ts(modified) — Now acceptsConstructiveOptionsand readsopts.captcha.recaptchaSecretKeyinstead ofprocess.env.RECAPTCHA_SECRET_KEY.Other changes:
AuthSettingsinterface andAUTH_SETTINGS_SQLexpanded withenableCookieAuthandrequireCsrfForAuthreq.tokenSource('bearer' | 'cookie' | 'none') set in auth middleware for downstream usecookie-parser,@constructive-io/csrf,@constructive-io/oauthadded as server dependenciesserver.tsin the correct order:cookieParser → api → OAuth routes → authenticate → CSRF setToken → CSRF protect → captcha → cookie lifecycle → graphileEnv var convention refactoring
All new middleware follows the workspace convention of zero direct
process.envaccess:OAuthOptions,OAuthProviderCredentials,CaptchaOptionsadded toConstructiveOptions(ingraphql/types)OAUTH_*(12 env vars) andRECAPTCHA_SECRET_KEYare now parsed ingetGraphQLEnvVars()(graphql/env/src/env.ts) — same pattern asSMTP_*,API_*, etc.getNodeEnv(): Allprocess.env.NODE_ENVchecks incookie.ts,csrf.ts,oauth.ts,captcha.ts, andserver.tsreplaced withgetNodeEnv()from@pgpmjs/envopts:oauth.tsreads providers/baseUrl/redirects fromopts.oauth.*;captcha.tsreads the secret key fromopts.captcha.recaptchaSecretKeyprocess.envreferences in any new middleware filegrafserv response interception
Research into grafserv (PostGraphile v5) source confirmed it never calls
res.json(). Instead, it writes responses directly via:The cookie middleware therefore:
res.writeHead()— captures statusCode and headers without sending themres.end()— parses the JSON body from the buffer, extractsaccessToken/deviceId, buildsSet-Cookieheader values manually viaserializeCookie()/serializeClearCookie()helperswriteHead+endtogetherReview & Testing Checklist for Human
writeHead/enddeferral incookie.ts(~lines 252–330): If any grafserv code path callsres.end()without first callingres.writeHead(),deferredStatusCodewill beundefinedand the interceptor will skipwriteHeadentirely, falling through tooriginalEnd— this should be safe (Node.js auto-sends headers), but verify no edge cases produce malformed responses. Also confirmContent-Length(set by grafserv based on body buffer size) remains accurate afterSet-Cookieheaders are injected into the headers object (it should, sinceContent-Lengthrefers to the body, not headers).req.body.operationNameto identify auth mutations. This requires the body to be parsed before this middleware runs, and the client must sendoperationNamein the request. Unnamed mutations will silently skip cookie-setting — confirm this is acceptable or if a fallback (AST parsing) is needed.email_verifiedhardcoded tofalse(oauth.tsline 80): The SSO flow always passesfalseforemail_verifiedtosign_in_sso(). Providers like Google do return verified email status — confirm whether this conservative default is acceptable or if provider-specific logic is needed.WeakMap<object, Request>pattern (oauth.ts~line 148): The request is stashed keyed onreq.queryto pass it into theonSuccesscallback. If the OAuth package reconstructs the query object rather than preserving the same reference, the lookup will silently fail andsign_in_sso()won't be called. Verify with an actual OAuth flow.graphql/env/src/env.ts): Onlygoogle,github,facebook,linkedinare parsed from env vars. Adding a new provider requires a code change ingetGraphQLEnvVars(). Confirm this is acceptable vs. a dynamic scan approach.cookie.tsline ~297): Not configurable viaapp_auth_settings. Confirm this is acceptable for now or should be a DB-configurable value.Recommended test plan: Enable
enable_cookie_authandrequire_csrf_for_authin a tenant'sapp_auth_settings, then:signInmutation with Bearer token → verify no cookie is set, CSRF is not enforcedsignInmutation without Bearer token → verifyconstructive_sessioncookie is set in responsesignOut→ verify session cookie is clearedNotes
cookie-parser,@types/cookie-parser, and workspace links forcsrf/oauth).parseIntervalToMs,serializeCookie/serializeClearCookie, the cookie extraction helpers, and the CSRF skip logic.WeakMap<object, Request>keyed onreq.queryto pass the Express request into theonSuccesscallback — this is a workaround for the OAuth package's callback API not exposing the raw request.process.envusage inupload.ts(MAX_UPLOAD_FILE_SIZE) andgraphile.ts(NODE_ENVforexplain) are out of scope for this PR.Link to Devin session: https://app.devin.ai/sessions/12acfda2a5434d2686c63515cfeb2610
Requested by: @pyramation