Skip to content

Secret-store entries aren't scoped by OAuth state file: profiles sharing a server overwrite each other's tokens #2549

Description

@cliffhall

Problem

Raised by Copilot on the v2.9.0 milestone-merge PR (#2536, review comment), and confirmed in the code.

#2481 moved acquired OAuth tokens out of the OAuth state file (oauth.json) and into the secret store. The secret store is not scoped by which state file is in use:

  • Its location ignores MCP_INSPECTOR_OAUTH_STATE_PATH. It is the OS keychain by default, one per user. The file backend is MCP_INSPECTOR_SECRET_FILE, else $MCP_STORAGE_DIR/secrets.json, else ~/.mcp-inspector/secrets.json (core/auth/node/secret-store-selection.ts:126-142).
  • Its entries are keyed only by server URL or issuer (oauthSecretServerId → oauth+<encoded url>, core/auth/node/oauth-secrets.ts:78).

So two CLI/TUI profiles that point MCP_INSPECTOR_OAUTH_STATE_PATH at different state files, but connect to the same server, now share one secret-store entry. When profile B logs in, it overwrites profile A's tokens. Profile A's state file is then joined with B's credentials, so A silently acts as B's identity. Before #2481, tokens lived inside each state file, so profiles were isolated. This is a regression from #2481.

Who is affected

Only people who set MCP_INSPECTOR_OAUTH_STATE_PATH to keep separate OAuth profiles against the same server. The default single-profile setup is unaffected.

docs/cli-smoke-testing.md recommends MCP_STORAGE_DIR + MCP_INSPECTOR_OAUTH_STATE_PATH for isolation. With the default keychain backend, that setup now shares tokens with the user's real keychain entries for the same server URL.

Workaround (until fixed)

Give each profile its own secret store as well as its own state file:

export MCP_INSPECTOR_SECRET_STORE=file
export MCP_INSPECTOR_SECRET_FILE=/path/to/profile-a/secrets.json   # distinct per profile

A per-profile MCP_STORAGE_DIR, with MCP_INSPECTOR_SECRET_STORE=file, also works.

Fix

Namespace secret IDs by the persistence context, for example a stable hash of the resolved state-file path when it is not the default, with a migration for existing unscoped IDs, or otherwise restore per-state-file isolation. Constraints:

  • IDs must stay colon-free and must not prefix-collide. The keyring store's deleteAllForServer parses accounts at the first colon (see the comment above oauthSecretServerId).
  • The default single-profile path must keep its current IDs, or migrate them transparently, so existing users are not logged out.

Update docs/cli-smoke-testing.md (and docs/environment-variables.md) in the same change so that the documented isolation recipe is isolated again.

Acceptance

  • Two profiles with different MCP_INSPECTOR_OAUTH_STATE_PATH values against one server keep separate tokens, on both the keyring and file backends
  • Existing default-profile tokens survive the upgrade (migration tested)
  • The smoke-testing isolation recipe is correct as documented

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingv2Issues and PRs for v2

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions