fix(oauth): align MCP scope boundary - #222
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
The new dashboard step-up enforcement relies on client-supplied MCP headers for authorization decisions, which can allow requests to bypass the intended HTTP-layer insufficient-scope challenge behavior.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR updates Fanout’s MCP OAuth contract so OAuth scopes (not account roles) define the delegated authorization boundary, introducing canonical scopes (telemetry:read, dashboard:manage) while keeping legacy fanout:* scopes working via canonicalization.
Changes:
- Canonicalize/validate MCP OAuth scopes (including legacy aliases) and enforce refresh-token narrowing (no scope expansion).
- Remove delegated account role exposure from MCP
TokenInfo.Extra, and re-check live user capabilities when verifying delegated tokens. - Advertise updated OAuth discovery metadata (canonical scopes, protected-resource name, RFC 9207
iss), and update docs/tests accordingly.
File summaries
| File | Description |
|---|---|
| site/src/content/docs/guides/connect-over-mcp.mdx | Updates public MCP docs to reflect canonical scopes and scope-based delegation boundary. |
| internal/dashboard/identity.go | Switches dashboard OAuth scope constant to canonical dashboard:manage. |
| internal/auth/oauth_store.go | Canonicalizes scopes at issuance/verification and enforces refresh scope narrowing. |
| internal/auth/oauth_store_test.go | Adds regression coverage for legacy-scope canonicalization and updates refresh rotation call sites. |
| internal/auth/oauth_scope.go | Introduces canonical scope parsing + legacy alias handling and refresh narrowing policy. |
| internal/auth/oauth_scope_test.go | Adds unit tests for canonicalization and refresh narrowing/expansion rejection. |
| internal/api/oauth.go | Updates MCP OAuth endpoints/metadata, removes delegated role exposure, adds iss, and adds HTTP-layer scope challenges. |
| internal/api/oauth_test.go | Expands OAuth/MCP regression tests for discovery metadata, iss, role non-exposure, and scope enforcement. |
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
The new HTTP-layer per-tool scope gate can fail open for oversized/unparseable MCP request bodies, which can break the promised 403 + insufficient_scope challenge behavior for dashboard tool calls in edge cases.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
The new MCP authorization-body parser currently treats JSON null / empty JSON-RPC batches as valid, which can bypass the intended “invalid MCP request body” 400 response from the authorization gate.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Review details
- Files reviewed: 8/8 changed files
- Comments generated: 1
- Review effort level: Lite
What changed
Make OAuth scopes the MCP delegation boundary with canonical
telemetry:readanddashboard:managenames. Existingfanout:readandfanout:dashboardgrants remain accepted and are canonicalized in memory oron token rotation, so registrations and active credentials are not
invalidated.
Delegated tokens no longer expose the account role, and authorization checks
map scopes to the user's live capabilities. Dashboard tool calls require
dashboard:manageand return aninsufficient_scopechallenge when aread-only token attempts them.
OAuth discovery now advertises the canonical scopes, protected-resource name,
and RFC 9207 issuer support; authorization redirects include
iss. The tokenendpoint rejects
scopeon authorization-code exchange and permits refreshtokens to retain or narrow—but never expand—the original grant without
consuming a token on
invalid_scope. Public MCP documentation and regressioncoverage are updated with the new contract.
Closes #221
Contract notes:
telemetry:readanddashboard:manage.client deletion, forced re-registration, or forced re-consent.
when the token lacks
dashboard:manage.issquery parameter.Verification
just checkjust test-racewhen auth, API, ingest, query, MCP, or agent paths changedAdditional checks performed during implementation included focused OAuth,
auth, MCP, and dashboard tests plus
go vet ./....