Skip to content

fix(auth): sign dev-session cookie + audit-log dev-bypass attribution + isolation doc#3471

Merged
bokelley merged 3 commits into
mainfrom
bokelley/dev-mode-security-hardening
Apr 29, 2026
Merged

fix(auth): sign dev-session cookie + audit-log dev-bypass attribution + isolation doc#3471
bokelley merged 3 commits into
mainfrom
bokelley/dev-mode-security-hardening

Conversation

@bokelley

Copy link
Copy Markdown
Contributor

Summary

Three security-medium hardening items deferred from the PR #3448 review.

  1. Sign the dev-session cookie. Pre-PR the cookie was the literal user-key string (admin, member); anyone who could set a cookie on the domain could pick a privileged dev user. Now HMAC-signed with a per-process secret generated at boot — invalid signatures fail verification on read, and cookies don't survive a server restart. Format: ${userKey}.${base64url-hmac}. Timing-safe comparison.

  2. Audit-log dev-bypass attribution. resolveUserOrgMembership now returns via_dev_bypass: boolean. Routes that write to registry_audit_log tag dev-bypass writes with details.auth_method = 'dev-bypass' so post-incident triage can distinguish them from real-user writes (synthetic user_dev_* IDs don't resolve via WorkOS). Applied to the highest-impact mutating routes (org rename, settings PATCH); pattern documented for new audit-log writers.

  3. ops/dev-mode-isolation.md — operational invariants the bypass assumes (no shared dev/prod DB, no shared .env.local, prod-boot guard verification on every deploy). Incident response steps if dev mode ever activates in prod.

Test plan

  • npm run typecheck clean
  • 6 new unit tests for cookie signing (round-trip, legacy format rejected, tampered sig rejected, forged-user-key-with-valid-sig-from-other-key rejected, unknown user rejected, empty/missing rejected)
  • 86/86 pass on the impacted integration suite (resolve-user-org-membership.test.ts updated for the new via_dev_bypass field; membership-webhook, find-paying-org-for-domain, primary-organization-resolution unaffected)
  • Live verification: signed cookie still authenticates dev admin in browser; legacy dev-session=admin no longer authenticates

🤖 Generated with Claude Code

bokelley and others added 3 commits April 28, 2026 14:40
… + isolation doc

Three security-medium items deferred from PR #3448 review:

- Sign the dev-session cookie. Was the literal user key string ("admin"
  / "member"); anyone who could write a cookie on the domain (XSS,
  sibling subdomain) could pick a privileged dev user. Now HMAC-signed
  with a per-process secret generated at boot — invalid signatures fail
  verification on read. Format: ${userKey}.${base64url-hmac}. Timing-
  safe comparison. Cookies don't survive a server restart, which is the
  desired property: a cookie minted on someone else's box won't work on
  yours.
- Audit-log dev-bypass attribution. resolveUserOrgMembership now returns
  via_dev_bypass: boolean. Routes that write registry_audit_log tag
  dev-bypass writes with details.auth_method = 'dev-bypass' so post-
  incident triage can distinguish them from real-user writes (synthetic
  user_dev_* IDs don't resolve in WorkOS). Applied to org rename + org
  settings PATCH; pattern documented for new audit-log writers.
- New ops/dev-mode-isolation.md doc — operational invariants the bypass
  assumes (no shared dev/prod DB, no shared .env.local, prod-boot guard
  verification on every deploy). Incident response steps if dev mode
  ever activates in prod.

Tests: 6 unit tests for cookie signing (round-trip, legacy format
rejected, tampered sig rejected, forged user-key with valid sig from
other key rejected, unknown user rejected, empty/missing rejected).
86/86 pass on the impacted integration suite.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The dev-session-signing test had a vi.mock with `...actual` spread —
effectively a no-op, but vi.mock factories are hoisted ahead of
setupFiles, so the auth.js import via importOriginal evaluated with
WORKOS_API_KEY undefined and crashed at the top-level
`new WorkOS(process.env.WORKOS_API_KEY!)`. Drop the mock; setupFiles
sets the mock API key in time when the import runs normally.

Plus merge origin/main: the audit-log-IP/UA capture from main and the
dev-bypass tagging from this branch combine cleanly in details.
…need WorkOS env

CI runs from repo root and picks up the root vitest.config.ts (no
setupFiles), not server/vitest.config.ts. So the WORKOS_API_KEY mock that
makes auth.ts loadable in local tests isn't set in CI. Stub the WorkOS
module directly so the import doesn't depend on env-var ordering.
@bokelley bokelley merged commit 87ae56d into main Apr 29, 2026
13 checks passed
@bokelley bokelley deleted the bokelley/dev-mode-security-hardening branch April 29, 2026 02:05
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