Skip to content

fix(worker): narrow unsupported MCP OAuth scopes at authorize (offline_access) - #298

Closed
outof-place wants to merge 1 commit into
mainfrom
fix/mcp-oauth-offline-access-scope
Closed

fix(worker): narrow unsupported MCP OAuth scopes at authorize (offline_access)#298
outof-place wants to merge 1 commit into
mainfrom
fix/mcp-oauth-offline-access-scope

Conversation

@outof-place

Copy link
Copy Markdown
Collaborator

Problem (AIW-282 / AIW-272)

Connecting a native MCP client (Claude Code) to the Arthur tenant fails at the OAuth callback with:

error=invalid_scope&error_description=The following scopes are invalid: offline_access

Claude Code's native OAuth requests offline_access (to obtain a refresh token). offline_access is not in MCP_SCOPES, so at /oauth2/authorize the provider (@better-auth/oauth-provider@1.6.20, dist/index.mjs:3873-3880) finds an unknown scope and hard-rejects the whole authorization rather than degrading. No native client can connect. (refresh_token is advertised in grant_types_supported but offline_access was never grantable, so a client that asks for it is turned away outright — the sibling of AIW-282, where native clients also can't obtain write scopes.)

Fix — graceful scope narrowing (RFC 6749 §3.3)

Rather than adding full offline_access + refresh-token support (long-lived tokens, broader security surface, and revocation is already known to be a no-op on this stateless-JWKS deployment), this takes the least-risky path the task calls for: drop the scopes we cannot grant and proceed with the supported subset. RFC 6749 §3.3 explicitly permits the authorization server to issue a narrower scope than requested.

A new narrowMcpAuthorizeScope(path, query) runs in the existing Better Auth before-hook, ahead of the provider, and rewrites ctx.query.scope on the /oauth2/authorize request to keep only MCP_SCOPES. Because it mutates the same query object the provider then validates, signs into the consent redirect, and stores on the authorization code, the narrowed set is exactly what consent lists and what the issued token carries.

Security invariants preserved

  • No widening. It filters against MCP_SCOPES, not the client's registration, and only ever removes — a scope the client never registered still reaches the provider's own client-scope check and is still rejected there.
  • No fallback expansion. When nothing grantable remains, the scope collapses to "" (not deleted), so the provider does not fall back to the client's full registered default set.
  • Untouched paths. Gated to /oauth2/authorize only; client_credentials/no-sub write-scope stripping (in request-context.ts), Basic/DCR validation, PKCE, and flow binding are unchanged.
  • Consent honesty. Narrowing happens before oAuthState.set, so the signed consent redirect and the custom consent page (allowedScopes, also MCP_SCOPES-based) present only the scopes actually granted.

Verification

  • src/mcp/oauth.test.ts — 23/23 pass, incl. 5 new unit tests (drops offline_access, keeps supported subset, drops any unsupported scope, collapses-not-widens when only unsupported asked, leaves non-authorize paths untouched).
  • src/auth.test.ts — 19/19 pass, incl. a new integration test driving the real auth.handler on /oauth2/authorize?scope=mcp:read runs:dispatch offline_access: it now advances to login carrying only mcp:read runs:dispatch instead of redirecting with error=invalid_scope.
  • Red check: with the fix disabled, the new integration test reproduces the exact production symptom (?error=invalid_scope&error_description=The+following+scopes+are+invalid%3A+offline_access), confirming it is a genuine regression guard.
  • tsc --noEmit: zero errors in the changed files.

Deploy note

This is the shared worker codebase. The Arthur tenant must be redeployed (separate manual step, not part of this PR) before the dogfood connection is unblocked.

🤖 Generated with Claude Code

…e_access)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015kfeohXE66xx7RJPxZ2pvH
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@vercel

vercel Bot commented Aug 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ai-workflow-app-dashboard Ready Ready Preview Aug 17, 2026 8:44am

Request Review

@coderabbitai

coderabbitai Bot commented Aug 17, 2026

Copy link
Copy Markdown

Important

Review available on request

  • 🔍 Trigger review

Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment @coderabbitai review to review the latest changes. For a full review, comment @coderabbitai full review.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 52fc66ae-daae-47a9-9749-78854df62547


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@outof-place

Copy link
Copy Markdown
Collaborator Author

Superseded by #285 (feat(worker): issue refresh tokens via offline_access for MCP OAuth), which is now merged to main. #285 takes the correct approach: it advertises offline_access as a permission-inert, opt-in scope so the provider issues a 30-day refresh token, instead of stripping offline_access at authorize (which only moved the failure downstream to hourly re-auth, as the review flagged). Closing this one to avoid merging the opposite approach.

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