Skip to content

Scope MCP OAuth tokens to user + workspace (#222) - #223

Closed
paulocastellano wants to merge 3 commits into
mainfrom
cursor/mcp-oauth-workspace-scope-5fec
Closed

Scope MCP OAuth tokens to user + workspace (#222)#223
paulocastellano wants to merge 3 commits into
mainfrom
cursor/mcp-oauth-workspace-scope-5fec

Conversation

@paulocastellano

@paulocastellano paulocastellano commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Closes #222.

Summary

MCP OAuth tokens (Claude/ChatGPT connectors) are now scoped to user + workspace, matching personal API keys. Requests resolve the workspace from the token instead of the user's current_workspace_id, so a multi-workspace agent cannot silently act on the wrong tenant.

Decisions

Item Decision
Capture workspace at connect Bind to Auth::user()->current_workspace_id at consent (including silent re-consent) + membership check.
Persist across code→token oauth_auth_codes.workspace_id; listener decrypts League's encrypted code (auth_code_id) and copies workspace.
Refresh tokens Decrypt refresh_token → inherit that specific access_token_id's workspace.
Bind failure Revoke the token and throw invalid_grant (no success response with a dead token).
Null after backfill Fail closed in middleware.
Partial member removal RemoveMember revokes that user's tokens for the removed workspace via shared RevokeAccessTokens.
API key list/delete Filter by personalAccess() grant type.
Onboarding / settings UI Follow-up after #204 (not on main yet).

Review hardening

  • Decrypt auth-code / refresh-token payloads (League encrypts them).
  • Silent re-consent uses current workspace, not stale session.
  • Refresh inherits the refreshed token, not “latest for client”.
  • Unbindable grants fail with invalid_grant.
  • Shared revoke helper for access + refresh tokens.
  • Backfill eager-loads users/workspaces (no N+1).

Test plan

  • Encrypted auth-code binding (consent workspace wins over later switch)
  • Refresh inherits refreshed token workspace amid sibling grants
  • Silent re-consent after workspace switch
  • Auth code rejects non-member current workspace
  • Unbindable grant → invalid_grant + revoked token
  • Middleware: bound / null / non-member
  • Backfill bind / fallback / left-current / revoke / ignore PATs
  • RevokeAccessTokens revokes refresh tokens
  • API key list excludes workspace-bound MCP OAuth
  • RemoveMember revokes only that workspace's tokens
Open in Web Open in Cursor 

cursoragent and others added 3 commits August 2, 2026 14:00
Bind authorization-code grants to the authorizing workspace (via auth codes),
inherit workspace on refresh, resolve MCP/API requests from the token instead
of current_workspace_id, backfill existing grants, and revoke workspace tokens
when a member is removed.

Co-authored-by: Paulo Castellano <hello@paulocastellano.com>
Decrypt League auth-code and refresh-token request payloads to resolve
workspace_id correctly, prefer Auth::user() at consent (including silent
re-consent), and cover multi-workspace refresh / reconnect edge cases.

Co-authored-by: Paulo Castellano <hello@paulocastellano.com>
Fail the OAuth grant with invalid_grant when a workspace cannot be bound,
centralize access/refresh token revocation, and remove N+1 queries from the
MCP OAuth workspace backfill.

Co-authored-by: Paulo Castellano <hello@paulocastellano.com>
@paulocastellano

Copy link
Copy Markdown
Contributor Author

Superseded by #245 — reimplemented on current main after #241 (MCP settings / token model) landed. Closing this draft.

@paulocastellano

Copy link
Copy Markdown
Contributor Author

Closing in favor of #245.

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.

Scope MCP OAuth tokens to user + workspace (with backfill)

2 participants