Skip to content

[HYPER-293] fix(pds-core): rewrite sec-fetch-site: same-site on /oauth/authorize - #65

Merged
aspiers merged 3 commits into
mainfrom
e2e/sec-fetch-diagnosis
Apr 10, 2026
Merged

[HYPER-293] fix(pds-core): rewrite sec-fetch-site: same-site on /oauth/authorize#65
aspiers merged 3 commits into
mainfrom
e2e/sec-fetch-diagnosis

Conversation

@aspiers

@aspiers aspiers commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Add middleware in pds-core to rewrite sec-fetch-site: same-site to same-origin for GET /oauth/authorize requests from the trusted auth subdomain
  • Add e2e scenario that sends sec-fetch-site: same-site directly via HTTP to catch this regression regardless of CI domain topology
  • Replace DB migration v9 (DROP client_logins) with a no-op to prevent breaking emergency rollbacks
  • Add no-drop-tables rule to AGENTS.md
  • Add **/tsconfig.tsbuildinfo to .dockerignore to fix stale build cache in Docker

Fixes HYPER-293

Context

PR #21 changed the epds-callback to redirect through the stock @atproto/oauth-provider /oauth/authorize endpoint. On deployments where auth and PDS share a registrable domain (e.g. *.test.certified.app), browsers send sec-fetch-site: same-site on the 303 redirect chain from the auth subdomain. The upstream atproto code rejects same-site (it allows same-origin, cross-site, none).

CI never caught this because Railway's up.railway.app is on the Public Suffix List, making each Railway subdomain its own "site" — so cross-service requests are cross-site (allowed) rather than same-site (rejected). Transparent browser-level testing via Playwright's page.route() is not possible because sec-fetch-site is a browser-controlled header that cannot be overridden via CDP, and page.route() does not intercept redirect chain targets.

Test plan

  • Verify the e2e scenario fails against pr-base (before fix) — confirmed
  • Verify the middleware fix passes unit tests locally
  • Reproduce the error manually on local docker deployment with *.test.certified.app domain
  • Verify the e2e scenario passes in CI (after fix deployed)
  • Verify manual login works on local docker deployment with the fix

🤖 Generated with Claude Code

Summary by CodeRabbit

Bug Fixes

  • Fixed sign-in failures that occurred when the login service and data server shared the same domain name
  • Database migrations now preserve existing tables rather than removing them during upgrades

Tests

  • Added comprehensive end-to-end tests validating OAuth authorization endpoint security

Documentation

  • Updated database migration guidelines to explicitly prohibit destructive schema changes, such as dropping tables or columns

@vercel

vercel Bot commented Apr 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
epds-demo Ready Ready Preview, Comment Apr 10, 2026 6:54pm

Request Review

@coderabbitai

coderabbitai Bot commented Apr 10, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds an E2E security scenario and step definitions exercising PDS handling of sec-fetch-site, implements decision logic and Express middleware to conditionally rewrite sec-fetch-site for GET /oauth/authorize, and makes the v9 DB migration a no-op to preserve client_logins.

Changes

Cohort / File(s) Summary
E2E Config
e2e/cucumber.mjs
Added features/security.feature to exported Cucumber paths.
E2E Step Definitions
e2e/step-definitions/sec-fetch-site.steps.ts
New steps: obtain request_uri (PAR POST with fallback), send GET /oauth/authorize with sec-fetch-site header, record status/body, and assert response is not a 400 forbidden sec-fetch-site error.
E2E Feature
features/security.feature
New scenario (non-@pending) verifying GET /oauth/authorize with sec-fetch-site: "same-site" does not produce a 400 forbidden error; other scenarios marked pending.
Rewrite logic & Middleware
packages/pds-core/src/lib/sec-fetch-site-rewrite.ts, packages/pds-core/src/index.ts
Added shouldRewriteSecFetchSite decision function and secFetchSiteRewrite middleware; adjusted raw Express router stack insertion to place rewrite middleware before AS metadata override and updated startup log.
Unit tests (rewrite)
packages/pds-core/src/__tests__/sec-fetch-site-rewrite.test.ts
New Vitest suite covering permutations of method/path/referer/sec-fetch-site to validate rewrite decisions.
DB migration & tests/docs
packages/shared/src/db.ts, packages/shared/src/__tests__/db.test.ts, .changeset/consent-upstream-oauth-ui.md, AGENTS.md
v9 migration changed to a no-op (preserve client_logins); tests updated to expect table present; docs updated to forbid destructive schema drops and note migration behavior.
Changesets & CI
.changeset/sec-fetch-site-same-site-fix.md, .changeset/consent-upstream-oauth-ui.md
Added patch changeset documenting sec-fetch-site fix and v9 no-op; updated upgrade guidance.
Misc
.dockerignore
Added **/tsconfig.tsbuildinfo to Docker ignore patterns.

Sequence Diagram(s)

sequenceDiagram
  participant Browser as Browser (client)
  participant PDS as PDS Server (Express)
  participant Middleware as secFetchSiteRewrite
  participant Meta as asMetadataOverride
  participant Auth as Auth / PAR endpoint

  Browser->>PDS: GET /oauth/authorize?request_uri=...
  PDS->>Middleware: evaluate (method, path, sec-fetch-site, referer)
  Middleware-->>PDS: (maybe rewrite sec-fetch-site)
  PDS->>Meta: apply AS metadata override
  PDS->>Auth: POST /oauth/par (to obtain request_uri) / or use provided request_uri
  Auth-->>PDS: request_uri (or error)
  PDS-->>Browser: response (status + body / redirect)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Poem

🐇 I hopped to PAR to chase a tiny clue,
Sent sec-fetch-site and nudged the headers too,
Middleware whispered, "same-site → same-origin" light,
No 400 thump — the flow resumed tonight,
🥕 Tests pass, carrots dance — hooray for right!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding middleware to rewrite the sec-fetch-site header on the /oauth/authorize endpoint.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch e2e/sec-fetch-diagnosis

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@railway-app

railway-app Bot commented Apr 10, 2026

Copy link
Copy Markdown

🚅 Deployed to the ePDS-pr-65 environment in ePDS

Service Status Web Updated (UTC)
@certified-app/auth-service ✅ Success (View Logs) Web Apr 10, 2026 at 6:55 pm
@certified-app/demo ✅ Success (View Logs) Web Apr 10, 2026 at 6:54 pm
@certified-app/pds-core ✅ Success (View Logs) Web Apr 10, 2026 at 6:54 pm
@certified-app/demo untrusted ✅ Success (View Logs) Web Apr 10, 2026 at 6:54 pm

@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-65 April 10, 2026 15:36 Destroyed
@coveralls-official

coveralls-official Bot commented Apr 10, 2026

Copy link
Copy Markdown

Coverage Report for CI Build 24259042703

Coverage increased (+0.2%) to 31.705%

Details

  • Coverage increased (+0.2%) from the base build.
  • Patch coverage: 10 uncovered changes across 1 file (6 of 16 lines covered, 37.5%).
  • 1 coverage regression across 1 file.

Uncovered Changes

File Changed Covered %
packages/pds-core/src/index.ts 10 0 0.0%

Coverage Regressions

1 previously-covered line in 1 file lost coverage.

File Lines Losing Coverage Coverage
packages/pds-core/src/index.ts 1 0.0%

Coverage Stats

Coverage Status
Relevant Lines: 1671
Covered Lines: 554
Line Coverage: 33.15%
Relevant Branches: 950
Covered Branches: 277
Branch Coverage: 29.16%
Branches in Coverage %: Yes
Coverage Strength: 2.79 hits per line

💛 - Coveralls

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@e2e/step-definitions/sec-fetch-site.steps.ts`:
- Around line 74-83: The try/catch around res.text() and JSON.parse in
sec-fetch-site.steps.ts currently swallows errors and loses original body text,
and later comparisons against the string 'Forbidden' are case-sensitive; update
the logic in the block that sets this.lastHttpJson (the res.text() and
JSON.parse handling) to always preserve the raw body (e.g., include a property
like bodyRaw or keep body when JSON parsing fails), and ensure any caught errors
are logged at debug level instead of being ignored; additionally change the
'Forbidden' checks to use a case-insensitive comparison (e.g., normalize
toLowerCase()) wherever the code inspects this.lastHttpJson or response text so
sec-fetch-site rejections aren’t missed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 22de5a51-76fd-48d2-aecf-6821682f8979

📥 Commits

Reviewing files that changed from the base of the PR and between 7c08d41 and b8ee3de.

📒 Files selected for processing (1)
  • e2e/step-definitions/sec-fetch-site.steps.ts

Comment thread e2e/step-definitions/sec-fetch-site.steps.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/shared/src/__tests__/db.test.ts (1)

229-233: ⚠️ Potential issue | 🟡 Minor

Tighten schema-version assertion to cover v9.

Line 233 currently accepts >= 8, which can pass even if the v9 bump is skipped. This weakens regression detection for this v9-focused block.

Suggested test fix
-    expect(row.version).toBeGreaterThanOrEqual(8)
+    expect(row.version).toBeGreaterThanOrEqual(9)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/shared/src/__tests__/db.test.ts` around lines 229 - 233, The test
"schema version is at least 8 after migration" is too weak for the v9 bump;
update the assertion in the test (the block using db['db'].prepare('SELECT
version FROM schema_version').get()) to require version >= 9 (change
expect(row.version).toBeGreaterThanOrEqual(8) to
expect(row.version).toBeGreaterThanOrEqual(9)) and also update the test title
string to reflect v9 (e.g., "schema version is at least 9 after migration") so
the intent matches the assertion.
🧹 Nitpick comments (2)
packages/pds-core/src/index.ts (1)

24-24: Import ordering deviation.

Per coding guidelines, imports should be ordered: Node built-ins → external packages → internal workspace packages → local relative imports. This local import is placed before the node:crypto import at line 27.

♻️ Suggested reorder

Move the local import after the external packages:

 import { applyPdsPortFallback } from './lib/resolve-port.js'
-import { shouldRewriteSecFetchSite } from './lib/sec-fetch-site-rewrite.js'
 
 import type * as http from 'node:http'
 import { randomBytes, timingSafeEqual, createHash } from 'node:crypto'
 import { PDS, envToCfg, envToSecrets, readEnv } from '@atproto/pds'
 import { HandleUnavailableError } from '@atproto/oauth-provider'
 import {
   generateRandomHandle,
   createLogger,
   verifyCallback,
   escapeHtml,
   validateLocalPart,
   resolveClientMetadata,
 } from '@certified-app/shared'
+import { shouldRewriteSecFetchSite } from './lib/sec-fetch-site-rewrite.js'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/pds-core/src/index.ts` at line 24, The import for the local module
shouldRewriteSecFetchSite is out of order; reorder imports to follow Node
built-ins → external packages → internal workspace packages → local relative
imports by moving the "import { shouldRewriteSecFetchSite } from
'./lib/sec-fetch-site-rewrite.js'" line so it appears after the node:crypto
import (and after other external imports), ensuring import order conforms to
project guidelines.
e2e/step-definitions/sec-fetch-site.steps.ts (1)

22-23: Use String.raw to avoid manual escaping.

SonarCloud flags that String.raw should be used for the regex-like pattern with backslashes.

♻️ Suggested fix
 When(
-  'a GET request is sent to the PDS \\/oauth\\/authorize with sec-fetch-site {string}',
+  String.raw`a GET request is sent to the PDS /oauth/authorize with sec-fetch-site {string}`,
   async function (this: EpdsWorld, secFetchSiteValue: string) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@e2e/step-definitions/sec-fetch-site.steps.ts` around lines 22 - 23, Replace
the manually escaped step pattern in the When call with a raw template literal:
update the When(...) invocation that currently uses 'a GET request is sent to
the PDS \\/oauth\\/authorize with sec-fetch-site {string}' to use String.raw`a
GET request is sent to the PDS /oauth/authorize with sec-fetch-site {string}`
(i.e., reference the When step definition and change the step string to use
String.raw and remove the backslashes).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/pds-core/src/__tests__/sec-fetch-site-rewrite.test.ts`:
- Around line 1-74: Prettier formatting errors are present in the test file
containing the BASE constant and describe suite for shouldRewriteSecFetchSite;
run your formatter (e.g., run prettier --write on this file) or apply the repo's
Prettier rules to reformat the file so imports, spacing, and trailing commas
match project style, ensuring the BASE declaration, import line, and the
describe/it blocks (including long expect calls) are wrapped and indented per
Prettier output.

---

Outside diff comments:
In `@packages/shared/src/__tests__/db.test.ts`:
- Around line 229-233: The test "schema version is at least 8 after migration"
is too weak for the v9 bump; update the assertion in the test (the block using
db['db'].prepare('SELECT version FROM schema_version').get()) to require version
>= 9 (change expect(row.version).toBeGreaterThanOrEqual(8) to
expect(row.version).toBeGreaterThanOrEqual(9)) and also update the test title
string to reflect v9 (e.g., "schema version is at least 9 after migration") so
the intent matches the assertion.

---

Nitpick comments:
In `@e2e/step-definitions/sec-fetch-site.steps.ts`:
- Around line 22-23: Replace the manually escaped step pattern in the When call
with a raw template literal: update the When(...) invocation that currently uses
'a GET request is sent to the PDS \\/oauth\\/authorize with sec-fetch-site
{string}' to use String.raw`a GET request is sent to the PDS /oauth/authorize
with sec-fetch-site {string}` (i.e., reference the When step definition and
change the step string to use String.raw and remove the backslashes).

In `@packages/pds-core/src/index.ts`:
- Line 24: The import for the local module shouldRewriteSecFetchSite is out of
order; reorder imports to follow Node built-ins → external packages → internal
workspace packages → local relative imports by moving the "import {
shouldRewriteSecFetchSite } from './lib/sec-fetch-site-rewrite.js'" line so it
appears after the node:crypto import (and after other external imports),
ensuring import order conforms to project guidelines.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8c2f8052-60f0-49dd-9507-de7c851befcd

📥 Commits

Reviewing files that changed from the base of the PR and between b8ee3de and b301657.

📒 Files selected for processing (10)
  • .changeset/consent-upstream-oauth-ui.md
  • AGENTS.md
  • e2e/cucumber.mjs
  • e2e/step-definitions/sec-fetch-site.steps.ts
  • features/security.feature
  • packages/pds-core/src/__tests__/sec-fetch-site-rewrite.test.ts
  • packages/pds-core/src/index.ts
  • packages/pds-core/src/lib/sec-fetch-site-rewrite.ts
  • packages/shared/src/__tests__/db.test.ts
  • packages/shared/src/db.ts
✅ Files skipped from review due to trivial changes (2)
  • .changeset/consent-upstream-oauth-ui.md
  • AGENTS.md
🚧 Files skipped from review as they are similar to previous changes (2)
  • e2e/cucumber.mjs
  • features/security.feature

Comment thread packages/pds-core/src/__tests__/sec-fetch-site-rewrite.test.ts
Add a new test to reproduce a bug; it is expected to fail:

The upstream @atproto/oauth-provider rejects sec-fetch-site: same-site on
GET /oauth/authorize. This breaks ePDS deployments where the auth service
and PDS share a registrable domain (e.g. *.certified.app), because the
browser sends same-site when following the 303 redirect chain from the
auth subdomain to the PDS.

CI didn't catch this because Railway's up.railway.app is on the Public
Suffix List, making cross-service requests cross-site (which is allowed)
rather than same-site. This scenario sends the header directly via HTTP
to catch the rejection regardless of CI domain topology.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aspiers
aspiers force-pushed the e2e/sec-fetch-diagnosis branch from cdcfa90 to 997f262 Compare April 10, 2026 17:53
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-65 April 10, 2026 17:53 Destroyed
@aspiers aspiers changed the title test(e2e): detect sec-fetch-site: same-site rejection on /oauth/authorize fix(pds-core): rewrite sec-fetch-site: same-site on /oauth/authorize Apr 10, 2026
@aspiers aspiers changed the title fix(pds-core): rewrite sec-fetch-site: same-site on /oauth/authorize [HYPER-293] fix(pds-core): rewrite sec-fetch-site: same-site on /oauth/authorize Apr 10, 2026
@aspiers
aspiers force-pushed the e2e/sec-fetch-diagnosis branch from a3be915 to 469d6d2 Compare April 10, 2026 18:29
Stale tsbuildinfo files from the host caused tsc --build to skip
compilation inside Docker, producing an empty dist/ directory.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aspiers
aspiers force-pushed the e2e/sec-fetch-diagnosis branch from 469d6d2 to 72579d3 Compare April 10, 2026 18:37
…horize

The upstream @atproto/oauth-provider rejects sec-fetch-site: same-site on
GET /oauth/authorize. This breaks ePDS deployments where auth and PDS
share a registrable domain (e.g. *.test.certified.app), because browsers
send same-site when following the 303 redirect chain from the auth
subdomain.

Changes:
- Add middleware in pds-core to rewrite sec-fetch-site: same-site to
  same-origin for requests from the trusted auth subdomain
- Replace DB migration v9 (DROP client_logins) with a no-op to prevent
  breaking emergency rollbacks
- Update changeset to reflect that client_logins is kept but unused
- Add no-drop-tables rule to AGENTS.md
- Update db.test.ts to match new migration behavior

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@aspiers
aspiers force-pushed the e2e/sec-fetch-diagnosis branch from b9e32d0 to 313c071 Compare April 10, 2026 18:53
@railway-app
railway-app Bot temporarily deployed to ePDS / ePDS-pr-65 April 10, 2026 18:54 Destroyed
@sonarqubecloud

Copy link
Copy Markdown

@aspiers
aspiers merged commit 23da088 into main Apr 10, 2026
15 checks passed
@aspiers
aspiers deleted the e2e/sec-fetch-diagnosis branch April 10, 2026 19:03
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