fix(worker): narrow unsupported MCP OAuth scopes at authorize (offline_access) - #298
fix(worker): narrow unsupported MCP OAuth scopes at authorize (offline_access)#298outof-place wants to merge 1 commit into
Conversation
…e_access) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015kfeohXE66xx7RJPxZ2pvH
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review available on request
Reviews should be triggered manually for repositories with fewer than 10 stars. Select Trigger review above or comment ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 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. Comment |
|
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 |
Problem (AIW-282 / AIW-272)
Connecting a native MCP client (Claude Code) to the Arthur tenant fails at the OAuth callback with:
Claude Code's native OAuth requests
offline_access(to obtain a refresh token).offline_accessis not inMCP_SCOPES, so at/oauth2/authorizethe 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_tokenis advertised ingrant_types_supportedbutoffline_accesswas 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 Authbefore-hook, ahead of the provider, and rewritesctx.query.scopeon the/oauth2/authorizerequest to keep onlyMCP_SCOPES. Because it mutates the samequeryobject 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
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.""(not deleted), so the provider does not fall back to the client's full registered default set./oauth2/authorizeonly;client_credentials/no-subwrite-scope stripping (inrequest-context.ts), Basic/DCR validation, PKCE, and flow binding are unchanged.oAuthState.set, so the signed consent redirect and the custom consent page (allowedScopes, alsoMCP_SCOPES-based) present only the scopes actually granted.Verification
src/mcp/oauth.test.ts— 23/23 pass, incl. 5 new unit tests (dropsoffline_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 realauth.handleron/oauth2/authorize?scope=mcp:read runs:dispatch offline_access: it now advances to login carrying onlymcp:read runs:dispatchinstead of redirecting witherror=invalid_scope.?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