Skip to content

MCP OAuth: expired dynamic client registration is reused forever (invalid_client), no re-registration #5284

Open

Description

@rosea-eosys

Describe the bug

For remote HTTP MCP servers that use OAuth dynamic client registration (DCR), Copilot stores the registered client in ~/.copilot/mcp-oauth-config/<hash>.json and reuses it indefinitely. It ignores the registration's expiry and does not register again when the authorization server rejects the client.

GitBook-hosted docs MCP servers (https://<site>/~gitbook/mcp, authorization server https://sites.gitbook.com/oauth2/v1/<site_id>) issue registrations that expire after 7 days. The /register response includes client_id_issued_at and client_secret_expires_at, and the difference is exactly 604800 seconds. The cached JSON only stores issuedAt, not client_secret_expires_at. After 7 days, every sign-in attempt fails at /authorize (and /token) with:

{"error":"invalid_client","error_description":"Client authentication failed- Invalid client <client_id>"}

Clicking Sign in again (in the Copilot app: Customize → Installed → Sign in) gets the same error, because the expired client is reused. The only way to recover is to delete or move the cached <hash>.json and <hash>.verifier files by hand and restart. A new registration then works immediately.

Affected version

GitHub Copilot CLI 1.0.102-0.unstable.r40141230562.g130d689 (via the GitHub Copilot app)

Steps to reproduce the behavior

  1. Configure a GitBook-hosted docs MCP server, for example:
    { "mcpServers": { "traksys-docs": { "type": "http", "url": "https://docs.parsec-corp.com/~gitbook/mcp" } } }
  2. Sign in. Copilot registers a client via DCR and caches it in ~/.copilot/mcp-oauth-config/.
  3. Wait more than 7 days (or check the registration's client_secret_expires_at).
  4. Sign in again. Authorization fails with invalid_client / Client authentication failed- Invalid client …, and retrying never recovers.
  5. Move the cached <hash>.json / <hash>.verifier aside and restart. Sign-in succeeds with a newly registered client.

To show that a new registration works while the cached one doesn't:

curl -s -X POST https://sites.gitbook.com/oauth2/v1/<site_id>/register \
  -H "Content-Type: application/json" \
  -d '{"client_name":"test","redirect_uris":["http://127.0.0.1:33418/callback"],"grant_types":["authorization_code","refresh_token"],"response_types":["code"],"token_endpoint_auth_method":"none"}'
# -> client_secret_expires_at - client_id_issued_at == 604800

Expected behavior

  • Store client_secret_expires_at from the DCR response (RFC 7591 §3.2.1) and register again before using a client that has expired.
  • If the authorization or token endpoint returns invalid_client for a dynamically registered (non-static) client, discard the cached registration, register again, and retry sign-in automatically.
  • At minimum, provide a way to clear one server's cached OAuth registration (for example, a "Reset sign-in" action in the app or a CLI command) so users don't have to find hash-named files by hand.

Additional context

Activity

  1. added
    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry
    and removed on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area:mcpMCP server configuration, discovery, connectivity, OAuth, policy, and registry

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions