Skip to content

fix(mcp/oauthlogin): pin mcp login's callback path, keep the port ephemeral - #1377

Open
reyortiz3 wants to merge 2 commits into
mainfrom
fix/mcp-login-fixed-callback
Open

fix(mcp/oauthlogin): pin mcp login's callback path, keep the port ephemeral#1377
reyortiz3 wants to merge 2 commits into
mainfrom
fix/mcp-login-fixed-callback

Conversation

@reyortiz3

@reyortiz3 reyortiz3 commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

mecated mcp login <server> could never actually complete against a standards-compliant authorization server for any profile it supports. selectMCPLoginServer only ever returns a server whose OAuth was populated by loadOAuthClient — which accepts exactly a preregistered confidential client or a CIMD client, never DCR (internal/cliconfig/mcpprofile.go). Both of those client kinds commit to a redirect_uri that must be registered ahead of time. But runMCPLogin left the login runtime on its random-callback-path, ephemeral-port default (ADR 0112) — a fresh path and port on every single invocation. RFC 8252 §7.3 loopback matching (which a standards-compliant AS implements) only tolerates a varying port; the path must match exactly. So the presented redirect_uri could never match anything registered in advance, for either supported client kind.

This surfaced while wiring Connector Gateway's embedded auth server for CIMD interop with mecatl (stacklok/stacklok-enterprise-platform#3843): the connector-gateway side is ready, but a real mecated mcp login against it would still fail on this.

Fix (revised after review discussion)

An earlier version of this PR reused oauthlogin.ExactRedirectURL (mecatui's remote-login constant), which fixes both the callback path and the port (18473). On reflection that reintroduces exactly what issue #522's original design required avoiding: "use an unguessable callback path". A well-known, fixed port is squattable — another local process can pre-bind it before the legitimate login starts, hijacking or denying the real callback.

Only the path actually needs to be fixed for a preregistered/CIMD client's redirect_uri to be statically registerable. RFC 8252 §7.3 loopback matching — which every AS this command talks to already implements, since neither reachable client kind goes through DCR — ignores the port on both sides of the comparison. So this PR instead adds oauthlogin.Options.PinCallbackPath: it fixes the callback to a well-known path but keeps binding an ephemeral port (127.0.0.1:0), exactly like the existing random-path default already does. mecated mcp login now uses this instead of ExactRedirectURL.

Net effect:

  • The redirect_uri path is now stable enough to pre-register in a CIMD document or a preregistered client.
  • The port stays unpredictable per run, preserving the squatting resistance the original design wanted — an attacker still can't know which port to pre-bind.
  • No port-contention regression either: concurrent mecated mcp login calls to different servers each get their own ephemeral port, same as before this PR.

oauthlogin.ExactRedirectURL itself is untouched and still available — cmd/mecatui/login.go's remote-login flow keeps using it, since a general-purpose OIDC target can't be assumed to implement RFC 8252 dynamic-port matching the way an MCP-shaped AS does.

Development stage

  • Implementation — narrow, mechanism-level fix; no separate plan PR
  • Human waiver of spine: Yes — directing human (this session) authorized skipping the Plan/Interface split, given the scope (one new Options field + a small Authorize refactor + a new test matrix, no new subsystem).

Contract linkage

  • Work classification: Bounded (a targeted addition to an existing, accepted mechanism — not a new architectural decision)
  • Classification rationale: oauthlogin.Options/ExactRedirectURL/the fixed-vs-random callback split already exist; this adds a third, narrower mode alongside them (PinCallbackPath) and one call site's choice of which mode to use. No new abstractions beyond that.
  • Decision record: ADR 0112 (accepted) — its Consequences section already names "profiles compatible with random loopback ports and paths" as the runtime's known limitation. This PR extends the runtime to also support a profile that needs a fixed path while preserving the ephemeral port property ADR 0112's own security posture (and issue Runtime-owned browser and loopback login for MCP OAuth #522's "unguessable callback path" requirement) relied on. It doesn't reverse anything ADR 0112 decided; ExactRedirectURL's fully-fixed mode (introduced later, for mecatui) is also untouched.

Issue relationship

Relates to stacklok/stacklok-enterprise-platform#3843 (Connector Gateway ↔ mecatl CIMD interop). No mecatl-side issue was filed for this specific gap before this PR.

Type of change

  • Bug fix

Test plan

  • Linting (task lint) — 0 issues, all modules
  • Offline test suite — targeted: go test ./cmd/mecated/... ./mcp/oauthlogin/... ./internal/app/... all pass. Full task test run also checked: its two failures (TestNoStaleMovedADRSlugs, TestToolHiveImportsStayBehindApprovedAdapterLeaves) are pre-existing artifacts of an unrelated nested worktree present on disk in this environment (.claude/worktrees/product-metrics-otel/), not caused by this change.
  • Offline demo (go run ./cmd/mecademo) — not applicable
  • Markdown changed — no
  • User docs/user-facing behavior changed — no docs updated in this PR; flagging as a possible follow-up if mecated mcp login's docs mention the callback shape
  • Guarded engine API affected — no, cmd/ and mcp/oauthlogin only
  • Landed plan strict acceptance trace — no plan for this bug fix
  • Final implementation review: /panel-review — not run in this session

Changes

File Change
mcp/oauthlogin/runtime.go Extract fixedCallbackPath; add Options.PinCallbackPath (fixed path, ephemeral port); extract resolveCallbackMode to keep Authorize's branch count under the gocyclo limit; validate RedirectURL/PinCallbackPath are mutually exclusive
mcp/oauthlogin/runtime_test.go Add TestPinCallbackPathUsesFixedPathEphemeralPort (proves path fixed, port varies across two runs), TestPinCallbackPathUnauthenticatedFloodDoesNotSpendAttempts (mirrors the equivalent ExactRedirect* test), TestPinCallbackPathAndRedirectURLAreMutuallyExclusive
cmd/mecated/mcplogin.go Use PinCallbackPath: true instead of RedirectURL: ExactRedirectURL
cmd/mecated/mcplogin_test.go Update the one test that asserted the old (random-path) behavior to assert PinCallbackPath is forwarded instead

User-facing change

mecated mcp login <server> now binds its OAuth callback listener on a fixed, well-known path (/oauth/callback) instead of a random one, but the port is still chosen by the OS at bind time, same as before this change — no port pinning, no port-contention risk. Operators registering a preregistered client or publishing a CIMD document for a mecatl-facing server should use http://127.0.0.1/oauth/callback (no port, or any port — matching ignores it) as the redirect_uri.

Special notes for reviewers

  • This PR went through two iterations in review: the first reused ExactRedirectURL (fixed path + fixed port), which a security-minded read flagged as reintroducing the port-squatting exposure issue Runtime-owned browser and loopback login for MCP OAuth #522 deliberately avoided. This version fixes only the path, keeping the port ephemeral, which should fully reconcile both concerns — please double check that reasoning holds.
  • No new ADR was written; I read this as adding a narrower variant to ADR 0112's already-accepted mechanism, not a new architectural decision. Happy to write one if reviewers disagree with that framing.

🤖 Generated with Claude Code

reyortiz3 and others added 2 commits September 10, 2026 17:26
Every server reachable through `mecated mcp login` already commits to a
client identity that must be registered ahead of time: selectMCPLoginServer
only returns servers whose OAuth was populated by loadOAuthClient, which
accepts exactly a preregistered confidential client or a CIMD client, never
DCR. Leaving oauthlogin.Options.RedirectURL empty put every such login on
the runtime's random-path, ephemeral-port default (ADR 0112) instead, so
the presented redirect_uri never matched what either client kind had
registered -- the login could never actually succeed against a
standards-compliant authorization server enforcing RFC 8252 loopback
matching (which tolerates only a varying port, never a varying path).

Set RedirectURL to the existing ExactRedirectURL constant instead, the
same fixed callback cmd/mecatui/login.go's remote-login path already uses.
Renamed and updated the one test that explicitly locked in the old
random-path behavior.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous commit reused ExactRedirectURL for mecated mcp login, which
fixes BOTH the callback path and the port. That reintroduces exactly the
local port-squatting exposure issue #522's original design required
avoiding ("use an unguessable callback path"): a well-known port is
predictable and can be pre-bound by another local process before the
legitimate login starts, hijacking or denying the real callback.

Only the PATH actually needs to be fixed for a preregistered/CIMD client's
redirect_uri to be statically registerable. RFC 8252 SS7.3 loopback
matching -- which every authorization server this login talks to already
implements, since neither client kind is reachable through DCR -- ignores
the port on both sides of the comparison. So the port can and should stay
ephemeral, exactly as the random-path default already does; only the path
needs to stop being randomly generated per invocation.

Add Options.PinCallbackPath: fixes the callback to fixedCallbackPath
while still binding "127.0.0.1:0" (OS-assigned port), extracted alongside
ExactRedirectURL's fully-fixed mode into resolveCallbackMode to keep
Authorize's branch count under the gocyclo limit. ExactRedirectURL itself
is untouched and stays available for a target that genuinely needs an
exact string match (e.g. cmd/mecatui/login.go's remote OIDC login,
which cannot assume its target implements RFC 8252 dynamic-port
matching). mecated mcp login now uses PinCallbackPath instead.

This also removes the port-contention regression the previous commit
introduced: concurrent `mecated mcp login` calls to different servers
each get their own ephemeral port again, same as before either commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@reyortiz3 reyortiz3 changed the title fix(cmd/mecated): pin mcp login's OAuth callback to a fixed redirect_uri fix(mcp/oauthlogin): pin mcp login's callback path, keep the port ephemeral Sep 10, 2026
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