github / github/copilot-cli

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

オープン
#4,203 コメント 1 件 リアクション 3 件 担当者 0 名 GitHub で見る
area:authentication area:mcp
主要言語
Shell
スター
11.2k
フォーク
1.9k
平均マージ
14時間 16分
マージ済み PR(30日)
6

説明

## 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.

コントリビューションガイド

コントリビューションガイドを開く

調査の方向性

rmcp streamable HTTP transport の OAuthChallenge パスと、"OAuth authentication required." をログに記録する CLI ハンドラーから始めます。~/.copilot/mcp-oauth-config/.tokens.json にあるキャッシュ済みトークンと、検出された token_endpoint を確認し、その後、refresh grant によってローテーションされたトークンが永続化され、サーバーに再接続できることを検証します。期限切れの access tokens がサイレントに更新され、インタラクティブなログインなしでサーバーのツールが読み込まれれば完了です。

索引モデルが issue の本文から書いたものです。

評価

技術スタック
rust
領域
authentication, cli
issue の種類
バグ
難易度
4/5
見積もり時間
3〜5日
活発さ
活発
明瞭さ
おおむね明確
初心者へのやさしさ
48/100

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。