Skip to content

MCP OAuth tokens for HTTP servers not reliably reused across sessions — duplicate cache-key entries force repeated re-auth #4695

Description

@DaveHolden2025

Describe the bug

For an HTTP-type MCP server that uses OAuth (PKCE, public client — clientSecret: null), Copilot CLI appears to mint a new token-cache entry under a different cache-key hash more often than expected, instead of finding and reusing the one still-valid cached token for that server.

Over about 3 weeks of normal use, I have 13 separate <hash>.tokens.json files in ~/.copilot/mcp-oauth-config/ that are all for the exact same server URL + OAuth scope, when I'd expect at most one entry that gets updated/refreshed in place. Only 1 matching <hash>.json client-registration file exists for that server, so the token-cache key and the client-registration key appear to diverge over time.

Two of the 13 token files were written in the same second and contain byte-identical expiresAt epoch values, which strongly suggests a single OAuth completion got persisted under two different cache-key hashes rather than one canonical entry being reused.

Separately (possibly independent, possibly compounding): none of the 13 cached tokens contain a refreshToken field — only accessToken + expiresAt. Observed access-token lifetime is consistently ~75–90 minutes. Since there's no refresh token, once it expires there is no silent-refresh path and the full interactive browser consent flow must run again. Combined with the cache-key issue above, the net effect looks like "the token expires in under an hour and I constantly have to re-authenticate," when it's really a mix of (a) a genuinely short-lived, non-refreshable token, and (b) the client sometimes failing to locate a still-valid cached token for the same server.

Affected version

1.0.80

Steps to reproduce the behavior

  1. Configure an HTTP MCP server that requires OAuth (registered in ~/.copilot/mcp-config.json with "type": "http", pointing at a server behind an internal API gateway).
  2. Authenticate once via the interactive browser flow.
  3. Use the tool across multiple CLI sessions (including separate worktree-backed sessions) over several days.
  4. Inspect ~/.copilot/mcp-oauth-config/*.tokens.json — instead of a single file per server updated in place, multiple files accumulate for the identical server URL/scope. Some have expiresAt values identical to the second with another file.
  5. In practice this surfaces as being prompted to re-authenticate noticeably more often than the access token's actual ~75–90 minute lifetime would suggest, i.e. sessions don't always find/reuse an already-valid cached token.

Expected behavior

  • A given MCP server's OAuth token cache entry should be looked up and updated using a single, stable cache key (e.g., derived only from server/resource URL + client ID), not one that can vary across otherwise-identical auth attempts for the same server.
  • When a still-valid, non-expired cached token already exists for a server, any session should find and reuse it rather than triggering a fresh interactive login.
  • If a variable value (such as the randomly chosen local loopback redirect port used during the OAuth flow) is unintentionally being folded into the persisted cache-key, that would explain the duplicate-key behavior observed above.

Additional context

  • OS: Windows 11 (Windows_NT)
  • MCP server type: http, OAuth with PKCE, public client
  • Token cache directory (~/.copilot/mcp-oauth-config/) is per-user/global, not per-session, so in principle tokens should already be shareable across sessions/worktrees on the same machine — but the duplicate cache-key entries above show that reuse isn't happening reliably.
  • Observed access-token lifetimes across 13 samples for the same server: consistently ~75–90 minutes.
  • No refreshToken field present in any of the 13 cached token files for this server.
  • Docs describe mcp-oauth-config/ as file-based storage used "when keychain-backed storage is unavailable." On this machine (Windows 11 with Credential Manager available) it may be worth confirming why the file-based fallback path is being used at all, since that could be related to the cache-key inconsistency.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions