anthropics / anthropics/claude-ai-mcp

iOS MCP connector does not silently refresh; every access token expiry forces interactive OAuth

未关闭
#319 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看
auth bug server-developer-report
主要语言
没有语言数据
星标
471
派生
76
PR 合并指标
30 天内没有已合并 PR

描述

### MCP server URL

https://vault-mcp.psky.us/mcp

### Where does the issue occur?

Other

### Transport used

streamable-http

### Client registration type

Dynamic Client Registration (DCR)

### SSE only: Does your server return the expected status code for POST requests?

Not applicable (my server does not use SSE)

### When did you last reproduce this?

2026-05-19, observed continuously between ~14:00 UTC and ~18:00 UTC (Pacific Time: ~7:00 AM – 11:00 AM PT). Three full interactive OAuth dances triggered at 14:26, 16:15, 17:59 UTC.

### Browser and OS

Native Claude iOS app, in-app OAuth modal. Microsoft Entra audit logs record the client as Mobile Safari 26.5 / iOS 18.7.0 (the in-app WebView reuses Safari's network stack).

### Describe the issue

The iOS Claude app's MCP connector does not perform silent token refresh on access token expiry. Every ~1h45m (≈ Microsoft Entra v2.0 default access-token lifetime), the user sees an "Authentication required to use this tool — Connect" modal and must complete a full interactive OAuth dance.

Per Anthropic's own documentation at https://claude.com/docs/connectors/building/authentication:

> "Claude also appends `offline_access` when your authorization server metadata lists it in `scopes_supported`, to obtain a refresh token."

> "Claude refreshes tokens reactively on a 401 response, with a proactive refresh up to five minutes before the stored expiry."

Neither documented behavior is occurring on iOS. The server advertises `offline_access` in `scopes_supported` (both PRM and AS metadata) and `refresh_token` in `grant_types_supported`/DCR response, but Microsoft Entra audit logs show zero `nonInteractiveUser` (refresh) sign-ins in a 4-hour window despite 51 successful tool calls. The OAuth flow simply restarts from the authorize endpoint every time, requiring the user to tap [Connect].

### Issue details

```shell
Reproduction:
1. Set up an MCP server backed by Microsoft Entra v2.0 OAuth, advertising `offline_access` in `scopes_supported` and `refresh_token` in `grant_types_supported`.
2. Connect to it from the iOS Claude app's MCP connector UI (vault-mcp.psky.us/mcp in our case).
3. Use a tool successfully.
4. Wait ~1h45m.
5. Attempt another tool call.
6. Observe: the "Authentication required to use this tool" modal appears. Tapping [Connect] kicks off a full interactive authorize → consent → token exchange.
7. Pull Microsoft Entra `auditLogs/signIns` for the app reg over the test window — zero `nonInteractiveUser` events.

No error ID was shown in the Claude UI — the modal is the only user-visible artifact. No 4xx/5xx errors were generated by the MCP server.
```

### Expected behavior

Per https://claude.com/docs/connectors/building/authentication:

1. On startup / connector use after access-token expiry, Claude should silently use the stored refresh token to obtain a new access token (proactive refresh ≤5 min before stored expiry).

2. If a 401 is received from the MCP server with a still-valid refresh token in storage, Claude should silently call the token endpoint with grant_type=refresh_token, obtain a fresh access token, and retry the original request.

3. The user should NOT see the "Authentication required" modal as long as the refresh token remains valid (publicClient refresh tokens are 90-day sliding by default on Microsoft Entra v2.0).

Concretely, per Microsoft Entra audit logs, we should be seeing periodic `nonInteractiveUser` (signInEventTypes=["nonInteractiveUser"]) entries for the vault-mcp app — at least one per ~hour of active use. We're seeing zero in 4-hour windows of continuous activity.

### Logs from your server

```shell
=== Server-side advertisement is correct ===

$ curl -s https://vault-mcp.psky.us/.well-known/oauth-authorization-server | jq '{scopes_supported, grant_types_supported}'
{
"scopes_supported": [
"https://vault-mcp.psky.us/mcp/access_as_user",
"offline_access"
],
"grant_types_supported": ["authorization_code", "refresh_token"]
}

$ curl -s -X POST https://vault-mcp.psky.us/register -H 'Content-Type: application/json' -d '{}' | jq '{scope, grant_types}'
{
"scope": "https://vault-mcp.psky.us/mcp/access_as_user offline_access",
"grant_types": ["authorization_code", "refresh_token"]
}

=== Microsoft Entra sign-in audit log (4-hour window, 2026-05-19 14:00–18:00 UTC) ===

Filter: appId eq '' and createdDateTime ge

| Time (UTC) | signInEventTypes | errorCode | OS / Browser |
|------------|-------------------|-----------|-----------------------------|
| 17:59:27 | interactiveUser | 0 | iOS 18.7.0 / Mobile Safari 26.5 |
| 16:15:45 | interactiveUser | 0 | iOS 18.7.0 / Mobile Safari 26.5 |
| 14:26:17 | interactiveUser | 0 | iOS 18.7.0 / Mobile Safari 26.5 |

Three interactive sign-ins over 3h33m. Zero nonInteractiveUser (refresh) events.

In the same window, the server processed 51 successful HTTP requests to /mcp
and 22 successful MCP tool calls — i.e., the connector was actively in use
between interactive sign-ins, and the refresh path simply never fired.

=== Per-user OAuth2 permission grant for offline_access ===

$ az rest --method GET --uri "https://graph.microsoft.com/beta/oauth2PermissionGrants?\$filter=principalId eq '' and resourceId eq ''"
{
"value": []
}

Zero per-user grants. If Claude had ever requested offline_access in the
authorize URL, Microsoft would have recorded a consent grant on first use.
This strongly suggests offline_access is not being included in the authorize
request at all.
```

### Additional context

=== References ===

- MCP SEP-2207 "OIDC-Flavored Refresh Token Guidance" (accepted Standards Track 2026-02-04):
https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/seps/2207-oidc-refresh-token-guidance.md
This SEP explicitly names Claude among the clients that were not requesting
refresh tokens via offline_access.

- MCP TypeScript SDK merged SEP-2207 client-side support on 2026-03-30.

- Community report (May 2026): "Custom MCP connectors worked reliably via
claude.ai web and iOS until approximately April 15, 2026, when the
Connections → Customize UI migration caused authentication failures."
(Per RedCaller MCP Client OAuth Refresh-Token Support matrix.)

=== Suspected root cause ===

Most likely: the iOS app's MCP OAuth flow builds the authorize URL without
including `offline_access` in the scope parameter. Without it, Microsoft
Entra never issues a refresh token, so no refresh is possible — explaining
both the absence of nonInteractiveUser audit events AND the absence of
per-user offline_access consent records.

Less likely: the iOS app does receive a refresh token but does not persist
it in Keychain or fails to read it for connector use.

=== Workaround in use ===

We applied a Microsoft Entra TokenLifetimePolicy with
AccessTokenLifetime=1.00:00:00 (24h) to the vault-mcp service principal,
reducing the user-visible modal frequency from ~14×/day to ~1×/day. This
masks but does not fix the missing-refresh behavior.

=== Server-side instrumentation ===

We added a DCR /register logging event on our server that captures the
grant_types, scope, redirect_uris, software_id, and User-Agent from each
client's registration request. If Anthropic confirms what Claude sends at
DCR time, we can correlate against server logs to pin the exact failure
point. Happy to share Entra audit log artifacts or DCR capture via secure
channel.

=== What we're asking ===

1. Confirm whether the iOS app's MCP OAuth flow currently includes
`offline_access` in the authorize URL when the server's scopes_supported
advertises it.
2. Confirm whether received refresh tokens are persisted (Keychain) and used
for silent refresh on access-token expiry.
3. If either is broken on iOS, fix to match the documented behavior at
claude.com/docs/connectors/building/authentication.

贡献指南

这个仓库没有索引到贡献指南

评估

这个 Issue 还没有评估数据。

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。