Harden MCP portal authentication changes - #199
Conversation
| connected: ServerAuthKind, configured: ServerAuthKind, | ||
| ): boolean { | ||
| return (connected === "token") !== (configured === "token"); | ||
| return configured === "oauth" ? connected === "token" : connected !== configured; |
There was a problem hiding this comment.
The doc-comment above (lines 165-168) is now stale relative to this new logic. It says probing may legitimately move between none and OAuth (bidirectionally), but configured === "oauth" ? connected === "token" : connected !== configured only tolerates that drift when the portal is configured as oauth. When configured as none, a connected oauth state now requires a reconnect (connected !== configured) — the intended new hardening, but it contradicts the comment. Please update the doc to describe the asymmetric rule (an oauth-configured portal may prove public during probing, while an explicitly none-configured portal stays strict).
|
Review submitted with one inline finding. I reviewed the PR (hardening MCP portal authentication). The logic is sound: the auth-mode reconnect rule, the OAuth fallback/callback gates, the connection-generation invalidation, and the per-revision in-flight counter all correctly implement "clear stale OAuth state / refuse paths no longer allowed / invalidate on token-authority change." The truth-table change to I submitted 1 inline finding: the doc-comment on |
972434f to
b4a5027
Compare
2846cdc to
f5c6473
Compare
|
I've completed my review. My analysis confirms the logic is sound across all six files:
The only actionable issue is the stale doc-comment on No new actionable findings. (1 pre-existing finding — the stale |
|
Now let me examine the Consider a portal reconfigured from
Now consider
The logic is coherent. Now let me double check one concern in I've completed a thorough review. The implementation is sound. The only actionable finding is the stale doc-comment on No new actionable findings. (1 pre-existing finding — the stale |
f5c6473 to
a6394d8
Compare
b4a5027 to
0ba0753
Compare
Preview:
|
0ba0753 to
79fa118
Compare
a6394d8 to
d90a456
Compare
79fa118 to
9380b8a
Compare
d90a456 to
7a2a950
Compare
9380b8a to
bd8a224
Compare
7a2a950 to
e97c4f4
Compare
bd8a224 to
41b0734
Compare
e97c4f4 to
a4c0bb9
Compare
41b0734 to
9edfd5f
Compare
a4c0bb9 to
5295e5d
Compare
2966f49 to
7efbbb0
Compare
5295e5d to
638ac69
Compare
638ac69 to
8624ef7
Compare
|
|
||
| export default defineConfig({ | ||
| plugins: [capnwebValidate()], | ||
| test: { |
There was a problem hiding this comment.
per sol
C1 — HIGH — vitest.config.ts:5-13 omits include, so the suite runs a stale shadow copy that asserts the opposite of this commit's fix
All three flagged this. Two independently produced runtime evidence. I confirmed it directly:
packages/gatekeeper-mcp-portal/vitest.config.ts → noinclude, noenvironment
packages/mcp-shared/vitest.config.ts (its source) → include: ["tests/*.test.ts"], environment: "node".wrangler/validate/tests/config.test.ts Aug 13 16:15 (pre-branch, collectible)
.wrangler/validate/src/config.ts:131 return (connected === "token") !== (configured === "token");
pnpm test:run reports 5 files / 67 tests where only 3 files exist; a clean checkout gives 3 / 38. The consequence is not merely noise — the run simultaneously asserts both answers for the same input:
- new tests/config.test.ts:150: portalAuthRequiresReconnect("oauth","none") → true
- stale .wrangler/.../config.test.ts:144: same call → false
So the package's own green suite proves nothing about the function this commit exists to change, and a future revert would be confirmed by a passing test. Compounding it: .wrangler/** is deliberately excluded from the test task's fingerprint (scripts/vitest-task-vite-config.ts), so a cached run can replay green over a snapshot nothing invalidates, and pnpm clean (rm -rf dist src/generated) never removes it.Fix is one line: include: ["tests/*.test.ts"] (plus environment: "node" for parity). Adding capnwebValidate() is correct and necessary — only the omissions are the problem.
|
|
||
| await expect(account.beginConnect("nonce", server)).resolves.toMatchObject({ kind: "redirect" }); | ||
|
|
||
| expect(values.get("portalConfigRevision")).not.toBe("old"); |
There was a problem hiding this comment.
this test is very light and doesn't really exercise the riskiest parts of this
Untested branches introduced by the diff: all four guards at portal.ts:328-329; both sides of the legacy previous === undefined branch; the rotation branch at 343-346; the #connectingRevisions refcount; allowsOAuthCallback (both outcomes, in either package); the portal's own allowsOAuthFallback (mcp-shared tests a hand-written fake instead); invalidateConnectionState() (no test invokes it at all); and — noted by agent 2 — portalAuthRequiresReconnect("token","token"), the single most common deployment configuration, asserted nowhere. A regression making it true bricks every token deployment silently.
| type ConnectedServer, | ||
| type ConnectOutcome, | ||
| } from "@gadgets/mcp-shared/account"; | ||
| import { McpAccount } from "../src/portal.js"; |
There was a problem hiding this comment.
🟡 The portal connector's tests cannot run on a freshly cloned or cleaned checkout
The new test pulls in the connector's worker module (../src/portal.js at packages/gatekeeper-mcp-portal/__tests__/account-revision.test.ts:8), which imports a build-generated file that only exists after the configurator build has run, so running just this package's tests on a clean tree fails to even load.
Impact: Anyone running the package's tests without a full prior build sees an unresolved-import failure instead of test results.
Generated configurator artifact is an undeclared prerequisite of the `test` task
packages/gatekeeper-mcp-portal/src/portal.ts:80 does import MCP_SERVER_CONFIGURATOR_HTML from "./generated/server-configurator-ui.txt". That path is gitignored (.gitignore:7) and is produced by the build:configurator Vite+ task (scripts/gatekeeper-configurator-vite-config.ts). The package's test task comes from withVitestTask(...) (scripts/vitest-task-vite-config.ts:118-133), which declares no dependsOn, and the test:run script is a bare vitest run. Until now no test in this package imported src/portal.ts, so the generated file was never needed at test time; the directory is in fact absent in this checkout. CI happens to be green because .github/workflows/ci.yml runs pnpm build before pnpm test, but the documented iteration path (pnpm --filter <pkg> test:run, vp run -F <pkg> test) breaks after pnpm clean or a fresh clone.
Prompt for agents
The new test suite in packages/gatekeeper-mcp-portal/__tests__/account-revision.test.ts imports ../src/portal.js, which statically imports ./generated/server-configurator-ui.txt. That file is gitignored and only produced by the package's build:configurator Vite+ task, so vitest fails to resolve the import on a clean tree (no src/generated directory). Options: make the package's `test` task depend on `build:configurator` (the package's vite.config.ts re-exports withTests from scripts/gatekeeper-configurator-vite-config.ts, so this needs a variant that adds dependsOn), or stub/alias the generated .txt module in packages/gatekeeper-mcp-portal/vitest.config.ts alongside the existing cloudflare:workers alias, or move the account/revision logic under test into a module that does not pull in the worker entry with its generated assets.
Was this helpful? React with 👍 or 👎 to provide feedback.
Portal authentication mode and deployment-owned tokens can change while an account or facet remains live. This makes auth-mode transitions clear stale OAuth state, refuses fallback/callback paths no longer allowed by current configuration, and invalidates captured connection generations when token authority changes.
OAuth redirects record their configuration revision before handing off to the callback, preventing an existing facet from invalidating the attempt while the user is authorizing.
Stacked on #170. Verified with the MCP and portal suites plus full
pnpm lint.