Skip to content

MCP OAuth connect: fix missing state param (CL-6371) - #115

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-6371-oauth-state
Aug 20, 2026
Merged

TheGreatAxios merged 2 commits into
mainfrom
cl-6371-oauth-state

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

Summary

  • PostHog's MCP OAuth connect died at the redirect with "Missing state parameter." Root cause: the MCP SDK's auth() only appends state= to the authorize URL when the OAuthClientProvider implements an optional state() method, and ours never did -- so every MCP OAuth connect (all presets share this one route) sent authorize requests with no state at all.
  • The route already minted a nonce and sealed it into the connect cookie, but never plumbed it into the SDK provider or validated it back on the callback -- CSRF protection that looked wired but did nothing.
  • Fix: mint the nonce before calling auth() on /start, hand it to the provider via session.state so the SDK sends it as state=, and on /callback require the provider's returned state to match exactly. Missing or mismatched state now redirects with a state_mismatch code through the existing outcome/code envelope, never the raw provider error text (CL-6360 idiom).
  • Verified against PostHog's real MCP OAuth metadata read-only (/.well-known/oauth-protected-resource/mcp, oauth.posthog.com/.well-known/oauth-authorization-server): standard authorization-code + PKCE (S256) flow, no credentialed requests made.
  • All 8 OAuth presets (PostHog, Granola, Linear, Notion, Sentry, Attio, Railway, Sumble) ride this single route, so the fix covers all of them.

Test plan

  • bun test in packages/connections (188 tests, 0 fail)
  • New test: authorize URL carries a non-empty state
  • New test: provider that echoes state back completes the connect
  • New test: provider that omits state we sent is rejected with code=state_mismatch, never the raw error text
  • tsc --noEmit and eslint clean on touched files

Cover the authorize URL carrying a CSRF-binding state param, a full
connect where the provider echoes it back, and a provider that omits
state we sent -- rejected as a CSRF failure via the consumer envelope,
never a raw provider error string.
The MCP SDK's auth() only appends ?state= to the authorize URL when
the OAuthClientProvider implements an optional state() method; ours
never did, so every MCP OAuth connect (PostHog and every other preset,
all sharing this one route) sent authorize requests with no state at
all. PostHog's authorization server rejects those with "Missing state
parameter." The route already minted a nonce and sealed it into the
connect cookie, but never plumbed it into the SDK provider or checked
it back on the callback -- CSRF protection that looked wired but did
nothing.

Fix: mint the nonce before calling auth() on /start, hand it to the
provider via session.state so the SDK sends it as state=, and on
/callback require the provider's returned state to match exactly.
Missing or mismatched state now redirects with a `state_mismatch`
error code through the same outcome/code envelope every other failure
here uses, never the raw provider message.
@TheGreatAxios
TheGreatAxios merged commit 9120652 into main Aug 20, 2026
0 of 2 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-6371-oauth-state branch August 25, 2026 15:29
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