fix(core): return OAuth discovery 401 for unauthenticated MCP POSTs#371
fix(core): return OAuth discovery 401 for unauthenticated MCP POSTs#371pejmanjohn wants to merge 7 commits intoemdash-cms:mainfrom
Conversation
🦋 Changeset detectedLatest commit: 56ce798 The changes in this PR will be included in the next version bump. This PR includes changesets to release 9 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
ascorbic
left a comment
There was a problem hiding this comment.
Thanks for the PR. The fix is in the right area, but I think there's a simpler approach. No MCP client will ever authenticate via session cookies, so we don't need to preserve CSRF for session-backed MCP requests: we can just make MCP bearer-only. I've made a couple of inline suggesitons, but it might need a few more as I've not tested. Basically for MCP requests we want to exempt from CSRF, but only allow token auth.
| const isOAuthConsent = url.pathname.startsWith("/_emdash/oauth/authorize"); | ||
| if ( | ||
| isApiRoute && | ||
| !isTokenAuth && | ||
| !isOAuthConsent && |
There was a problem hiding this comment.
| const isOAuthConsent = url.pathname.startsWith("/_emdash/oauth/authorize"); | |
| const isMcpEndpoint = url.pathname === "/_emdash/api/mcp"; | |
| if ( | |
| isApiRoute && | |
| !isTokenAuth && | |
| !isOAuthConsent && | |
| !isMcpEndpoint && |
There was a problem hiding this comment.
@ascorbic Updated this to match the broader bearer-only direction. MCP now short-circuits before session or external auth: requests without a valid bearer token return the discovery-style 401 with WWW-Authenticate, and session-backed MCP requests are no longer accepted even if the CSRF header is present.
I also updated the regression test to assert that MCP POST discovery no longer reads session state at all. Verified with pnpm --filter emdash test tests/unit/auth/mcp-discovery-post.test.ts, pnpm test tests/unit/auth/*.test.ts, pnpm typecheck, and pnpm --silent lint:json | jq .diagnostics | length.
| } | ||
| return response; | ||
| } | ||
|
|
There was a problem hiding this comment.
| if (isMcpEndpoint) { | |
| return Response.json( | |
| { error: { code: "NOT_AUTHENTICATED", message: "Not authenticated" } }, | |
| { status: 401, headers: { | |
| "WWW-Authenticate": `Bearer resource_metadata="${url.origin}/.well-known/oauth-protected-resource"`, | |
| ...MW_CACHE_HEADERS, | |
| }}, | |
| ); | |
| } |
What does this PR do?
Fixes OAuth discovery for unauthenticated MCP
POSTrequests on/_emdash/api/mcp.Some MCP clients, including Codex, probe the MCP endpoint with
POSTwhen starting auth discovery. Before this change, unauthenticatedPOST /_emdash/api/mcprequests withoutX-EmDash-Request: 1were rejected by the generic CSRF middleware with403 CSRF_REJECTEDbefore the MCP auth path could return401 Unauthorizedwith theWWW-Authenticateheader.That prevented OAuth-capable MCP clients from starting discovery even though EmDash already exposed valid OAuth metadata endpoints.
This change:
POSTrequests with no authenticated session to reach the existing401 + WWW-AuthenticateresponsePOSTrequests withoutX-EmDash-Request: 1POST, invalid bearer token handling, and the non-regression CSRF casesType of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0pnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
Screenshots / test output
Ran:
pnpm buildpnpm typecheckpnpm typecheck:demospnpm --filter emdash exec vitest run tests/unit/auth/mcp-discovery-post.test.ts tests/unit/auth/discovery-endpoints.test.ts tests/unit/api/csrf.test.ts tests/unit/mcp/authorization.test.tsNotes:
pnpm --silent lint:json | jq '.diagnostics | length'currently reports 3 existing warnings in unrelated files.