github / github/copilot-cli

Remote MCP (OAuth): expired access token forces interactive re-auth instead of silent refresh_token grant

Aperta
#4,203 1 commento 3 reazioni 0 assegnatari Vedi su GitHub
area:authentication area:mcp
Lingua principale
Shell
Stelle
11.2k
Fork
1.9k
Merge medio
14h 16m
PR unite (30g)
6

Descrizione

## Summary

When a remote OAuth-protected MCP server's cached **access token** is expired at session start, the Copilot CLI treats the server as needing a fresh **interactive** OAuth login and drops its tools — even though a valid **refresh token** is cached. It never attempts an [RFC 6749 §6](https://www.rfc-editor.org/rfc/rfc6749#section-6) `refresh_token` grant. In non-interactive/automation sessions the interactive flow can't complete, so the server's tools silently never load.

## Environment

- GitHub Copilot CLI `1.0.71`, running inside VS Code
- macOS (Darwin); credential store = Apple Keychain (`copilot-mcp-oauth`)
- MCP transport: `rmcp` streamable HTTP (`rmcp::transport::worker` / `::streamable`), `reqwest 0.13.4`

## Configuration

- Remote HTTP MCP server in `~/.copilot/mcp-config.json`:
```json
"notion": { "type": "http", "url": "https://mcp.notion.com/mcp" }
```
- Cached grant in Keychain (`service copilot-mcp-oauth`, account ``) and `~/.copilot/mcp-oauth-config/.tokens.json` — contains `accessToken`, `expiresAt`, `refreshToken`.
- Server authorization-server metadata ([RFC 8414](https://www.rfc-editor.org/rfc/rfc8414)) advertises refresh support:
`token_endpoint: https://mcp.notion.com/token`, `grant_types_supported: ["authorization_code", "refresh_token"]`

## Steps to reproduce

1. Authenticate a remote OAuth MCP server (e.g. Notion) so a valid access+refresh grant is cached.
2. Let the **access** token expire while the **refresh** token stays valid (Notion access tokens last ~8h — e.g. leave it overnight).
3. Start a new Copilot CLI session (especially a non-interactive/scheduled one).
4. Observe: the server's tools are absent from the session.

## Observed behavior

From `~/.copilot/logs/process-<...>.log` (redacted):

```
[ERROR] Starting remote MCP client for notion with url: https://mcp.notion.com/mcp
[ERROR] Connecting MCP client for notion...
-> connects to mcp.notion.com:443 using the cached (expired) access token
[ERROR] [rust:rmcp::transport::worker] worker quit with fatal: Transport channel closed,
when Client(OAuthChallenge { www_authenticate_header:
"...OAuth, resource_metadata=\".../.well-known/oauth-protected-resource/mcp\",
error=\"invalid_token\", error_description=\"Mis...\"" })
[ERROR] Server notion requires authentication, initiating OAuth flow
[ERROR] OAuth authentication required for notion
```

The runtime then reads the cached OAuth entry from Keychain but still ends at **"OAuth authentication required."**

### Evidence that no refresh was attempted

Grep of the entire session-start process log:

```
POSTs to mcp.notion.com/token : 0
```

Zero `refresh_token` grants were sent, despite a valid refresh token being cached.

## Expected behavior

On a `401` / `WWW-Authenticate: … error="invalid_token"` for a server that has a cached refresh token:

1. Silently POST `grant_type=refresh_token` (+ `client_id`) to the discovered `token_endpoint`.
2. Persist the rotated access/refresh tokens.
3. Reconnect and load the server's tools.
4. Only fall back to the interactive OAuth flow if the refresh itself fails (`invalid_grant`, expired/revoked).

## Impact

- Any remote OAuth MCP server silently loses its tools once the access token expires, until a manual re-auth — even though the refresh token is valid and the whole point of storing it is to avoid exactly this.
- Breaks unattended/scheduled sessions (e.g. a daily automation), where the interactive prompt can never be completed.

## Likely root cause / where to look

- The `rmcp` streamable transport surfaces the OAuth challenge as a **fatal** transport error ("worker quit with fatal … `OAuthChallenge`"), and the CLI's handler maps "auth required" directly to the interactive flow without first attempting a token refresh.
- Secondary: the session's tool list appears to be snapshotted at startup, so even a later successful (re)auth doesn't surface the tools into an already-running session.

## Suggested fix

- Insert a refresh step in the auth-challenge path: if a `refresh_token` is cached for the server, perform the refresh grant against the discovered `token_endpoint`, persist rotated tokens, and reconnect **before** considering interactive auth.
- Optionally refresh **proactively** when `expiresAt` is at/near expiry before the first connect.
- Consider re-syncing tools into active sessions after a late auth success.

## Workaround

- Manually perform the `refresh_token` grant against the `token_endpoint`, write the rotated tokens back into the cache; or re-run the CLI's interactive auth for the server before starting the session.

---

cc @connor4312 (VS Code team)

Filed with assistance from GitHub Copilot CLI.

Guida per i contributori

Apri la guida per i contributori

Direzione di ricerca

Inizia dal percorso OAuthChallenge del rmcp streamable HTTP transport e dal gestore CLI che registra "OAuth authentication required." Controlla i token memorizzati nella cache in ~/.copilot/mcp-oauth-config/.tokens.json e il token_endpoint individuato, quindi verifica che un refresh grant persista i token ruotati e riconnetta il server. Il lavoro è completato quando gli access tokens scaduti vengono aggiornati silenziosamente e gli strumenti del server vengono caricati senza un accesso interattivo.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
rust
Ambito
authentication, cli
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Attiva
Chiarezza
Abbastanza chiara
Idoneità per principianti
48/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.