Skip to content

Keep the static bearer token, and make it hold its weight (#114) - #129

Merged
TGoodhew merged 1 commit into
mainfrom
issue-114-static-bearer-decision
Jul 31, 2026
Merged

Keep the static bearer token, and make it hold its weight (#114)#129
TGoodhew merged 1 commit into
mainfrom
issue-114-static-bearer-decision

Conversation

@TGoodhew

Copy link
Copy Markdown
Owner

Closes #114. Decision: Option A — the HTTP transport authenticates with a static bearer token, not OAuth. Full reasoning is in the analysis comment; this PR records the decision and closes the gaps that made the current implementation weaker than a shared secret needs to be.

Why A

MCP authorization is OPTIONAL and explicitly SHOULD NOT be used on stdio — so this only ever concerned the HTTP transport (Copilot, ChatGPT), never Claude Desktop or the .mcpb bundle. Becoming a real OAuth resource server means an authorization server we'd have to stand up, a JWT stack in a 32-bit net472 binary, and — the part that actually blocks it — a stable public hostname, because OAuth binds a token to the server's canonical URI and publishes metadata beneath it. A tunnel name that changes on every restart breaks both. One bench, one bus, a tunnel opened deliberately: a shared secret is the right size.

Three fixes that stand on their own merits

1. A non-loopback bind with no token now refuses to start. It used to log a warning and serve anyway. Behind this endpoint is physical control of instruments, the remedy is one environment variable, and a warning in stderr is not a control. Loopback with no token still serves — that's the local development case.

2. The token comparison no longer returns early on the first differing byte. Over a tunnel the timing attack is poor, but a comparison that doesn't leak how much of the token was right costs nothing.

3. The Origin guard and tunnels interact in a way that was never written down. A server-side caller sends no Origin and passes — which is how the cloud connectors reach us — while a browser-based client on the tunnel's own domain sends one and is rejected. Correct as a DNS-rebinding defence; now documented as deliberate, with an explicit origin allow-list named as the fix if a browser client is ever wanted.

The documentation change that mattered most

packaging/chatgpt was recommending running with no token behind a public tunnel as the "testing" path. That was the genuinely dangerous advice in the repo — an unauthenticated public URL onto a live instrument bus. It now says plainly that ChatGPT is unsupported, why, and what to do instead: use Copilot, or put authentication at the edge (Cloudflare Access or similar), which buys the expiry and revocation a static token cannot.

I also moved the "no token configured" warning: it only fired for a non-loopback bind, but loopback is the case people actually tunnel, so that is where the reminder belongs. Binding loopback is not protection once the port is tunnelled, and the server cannot see that it has been.

Verification

  • 7 new tests — a network bind with no token throws with the variable named in the message; loopback with no token still serves; and a theory over token variants (exact, prefix, superset, wrong case, empty) pinning that only an exact match is accepted.
  • Full suite green: 414 GpibMcp + 113 Hpgl, 0 failures (Release|x86) — was 407 + 113.

README, packaging/templates/chatgpt.README.md.tmpl and Start-GpibMcpHttp.ps1 all updated so the decision is documented rather than rediscovered.

Decision recorded: the HTTP transport authenticates with a static bearer token,
not OAuth. MCP authorization is OPTIONAL and explicitly SHOULD NOT be used on
stdio, so this only ever concerned the HTTP transport - and becoming a real
OAuth resource server would mean an authorization server we would have to stand
up, a JWT stack in a 32-bit net472 binary, and a STABLE public hostname, because
OAuth binds a token to the servers canonical URI and publishes metadata beneath
it. A tunnel name that changes on every restart breaks both. One bench, one bus,
a tunnel opened deliberately: a shared secret is the right size.

Three fixes that stand on their own merits:

1. A non-loopback bind with no token now REFUSES TO START. It used to log a
   warning and serve anyway. What is behind this endpoint is physical control of
   instruments, the remedy is one environment variable, and a warning in stderr
   is not a control. Loopback with no token still serves - that is the local
   development case.

2. The token comparison no longer returns early on the first differing byte. Over
   a tunnel the timing attack is poor, but a comparison that does not leak how
   much of the token was right costs nothing.

3. The Origin guard and tunnels interact in a way that was never written down: a
   server-side caller sends no Origin and passes (which is how the cloud
   connectors reach us), while a BROWSER-based client on the tunnels own domain
   sends one and is rejected. Correct as a rebinding defence - now documented as
   deliberate, with an origin allow-list named as the fix if one is ever wanted.

The ChatGPT doc stopped recommending an unauthenticated public tunnel, which was
the genuinely dangerous advice in the repo: it now says plainly that ChatGPT is
unsupported, why, and what to do instead (Copilot, or authentication at the
edge, which buys the expiry and revocation a static token cannot).

Also corrected a warning that only fired for a non-loopback bind: the loopback
case is the one people actually tunnel, so that is where the reminder belongs.

7 tests.
@TGoodhew
TGoodhew merged commit 47e8b00 into main Jul 31, 2026
1 check passed
@TGoodhew
TGoodhew deleted the issue-114-static-bearer-decision branch July 31, 2026 19:09
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.

HTTP transport authorization: static bearer token vs the MCP authorization spec

1 participant