Remote MCP OAuth: shared user-scope refresh token breaks against IdPs with single-use refresh-token rotation (ODC)
Environment
- Claude Code 2.1.258, macOS (Darwin 25.6)
- Remote HTTP MCP server with OAuth via dynamic client registration (issuer
as-proxy, scopeopenid), credential stored at user scope in the macOS keychain (Claude Code-credentials→mcpOAuth.<server>) - IdP behind the server: OutSystems Developer Cloud (ODC) Identity Service
Summary
The IdP issues 5‑minute access tokens and single-use rotating refresh tokens; reusing a refresh token revokes the whole session. Claude Code stores one refresh token per server at user scope, and every concurrent session refreshes from it independently. With two or more Claude Code sessions open (CLI worktrees, Desktop, Cowork), the second refresh is treated as reuse, the IdP revokes the token family, and every session then reports:
MCP server "<name>" requires re-authorization (token expired)
Observed ≥6 forced re-authorizations in one working day. Never observed with a single active session, and never while one session was continuously calling tools (the refresh itself works: keychain expiresAt advanced from 22:37:27Z to 22:38:39Z between two reads one minute apart). Each re-auth mints a new DCR client.
IdP behaviour is documented by the vendor:
- https://success.outsystems.com/documentation/outsystems_developer_cloud/managing_outsystems_platform_and_apps/cloud_native_architecture_of_outsystems_developer_cloud/architecture_of_authentication_and_authorization_mechanism/#token-lifecycle-and-logout — "access tokens are short-lived JWTs with a five-minute lifetime … Refresh tokens are single-use … If the same refresh token is used a second time, the entire session is invalidated"
- https://success.outsystems.com/documentation/outsystems_developer_cloud/user_management/configure_user_session/#refresh-token-rotation — "If a previously used token is reused, ODC revokes all related tokens and terminates the session"
The Claude Code MCP docs describe the "refresh token rejection" notice but say nothing about sharing one OAuth credential across concurrent sessions, so this looks like an unhandled interaction rather than intended behaviour. (v2.1.206's transient-refresh fix is not the cause; 2.1.258 is affected.)
Side effect: a server-side stateful operation bound to the token (an in-flight Mentor run) is lost when the family is revoked.
Steps to reproduce
- Add an OAuth remote MCP server whose IdP rotates refresh tokens with reuse detection (ODC; or Keycloak with "Revoke Refresh Token" enabled and max reuse 0). Sign in once.
- Open two Claude Code sessions in the same user account.
- In session A call any tool. Wait > 5 min (access token expiry). In session B call any tool (B refreshes from the shared token and rotates it). Wait > 5 min. In session A call a tool again.
- Session A refreshes from the now-consumed token → IdP treats it as reuse → whole family revoked → both sessions show "requires re-authorization".
Expected
Any of:
- (a) Refreshes serialised through one owner/lock, with the rotated refresh token written back before any other session reads it, and sessions re-reading the store before refreshing.
- (b) Credentials keyed per session/client instead of one user-scope entry.
- (c) At minimum, documented guidance for IdPs with single-use rotation (e.g. "use one session per server").
Actual
Concurrent sessions silently invalidate each other's refresh token; the user must re-authenticate roughly every 30–90 minutes.