Skip to content

[MCP] Make OAuth lifecycle and reauthentication reliable for enterprise SSO #35006

Open

Description

@publi0

Summary

This is an umbrella/tracking issue for the end-to-end MCP OAuth lifecycle. It does not replace the narrower bug reports linked below.

Codex has already landed important foundations: shared credential-store locking and pinning (#30292 and #30293), serialized proactive refresh before startup and public operations (#32229), typed reauthentication signaling (#29877 and #30257), and runtime reconciliation primitives (#34930, #34952, and #34957).

The remaining problem is that these pieces do not yet produce one predictable recovery workflow across plugin projection, CLI/Desktop/IDE runtimes, refresh-failure classification, interactive reauthorization, and login/logout.

This is especially important in enterprise environments where many MCP servers sit behind corporate SSO, access tokens are intentionally short-lived, refresh tokens rotate, and several Codex processes or surfaces may share the same credential authority.

Claude Code documents that MCP OAuth tokens are stored securely and refreshed automatically. Teams evaluating migrations expect similarly predictable behavior from Codex; this issue does not require the same implementation, only equivalent lifecycle reliability:

https://code.claude.com/docs/en/mcp#authenticate-with-remote-mcp-servers

Current upstream state

The observations below were rechecked against main at ba42e6866 on 2026-07-30.

Upstream update — 2026-07-30

Since the previous review, upstream has landed:

  • #35280: an absent managed-plugin MCP allowlist no longer disables all plugin MCPs; an explicitly empty allowlist still denies them.
  • #35146, #35151, #35204, and #35239: runtime refresh/reconciliation, connection replacement, callback invalidation, and OAuth discovery through the configured HTTP client.
  • #36045: a distinct unknown MCP auth status when OAuth discovery fails or is inconclusive, preventing transient VPN, rate-limit, or provider errors from being reported as unsupported.
  • #36119, #36120, #36127, and #36168: centralized tool/runtime registration, MCP readiness waits, host-tool collision protection, and simpler telemetry plumbing.

These changes improve status accuracy and runtime composition, but they do not yet provide definitive refresh-failure classification, one-shot 401 session recovery, or cross-process adoption of an external CLI login.

Already landed

  • #30292 and #30293 serialize aggregate stores and pin the selected credential store for the client lifecycle.
  • #32229 performs a serialized read-refresh-write transaction, rereads the authoritative store after locking, adopts a winner from another process, persists token rotation before use, and runs refresh before MCP initialize and public operations.
  • #29877 and #30257 surface authentication-required startup failures through a typed reauthenticationRequired reason for the wrappers they recognize.
  • #34930, #34952, #34957, #35146, #35151, #35204, and #35239 provide runtime refresh, connection reuse/replacement, in-process OAuth callback invalidation, and configured-client OAuth discovery.
  • #35280 fixes the absent-plugin-MCP-allowlist behavior.
  • #36045 distinguishes inconclusive OAuth discovery (unknown) from confirmed lack of OAuth support (unsupported).

These are substantial improvements. The remaining gaps below should be framed as coverage and lifecycle integration on top of those foundations, not as if automatic refresh or reconciliation infrastructure were completely absent.

Remaining lifecycle gaps

1. Keep plugin MCP projection and management commands consistent

The absent-allowlist bug was fixed by #35280. The broader silent tool-omission symptom is also reported in #18527.

There is a second registry-parity problem in #34859: the running runtime can materialize an account-synced plugin MCP while a standalone codex mcp login <name> process cannot resolve the same server. Runtime, status, and management commands should use the same effective MCP registry.

A configured server should be mounted or expose a precise policy, registry, authentication, or startup reason. Policy-disabled state must not be presented as Auth: Unsupported.

2. Preserve automatic-refresh guarantees across every path

The baseline proactive refresh transaction is implemented by #32229. The remaining requirement is to ensure that routed/plugin/Desktop paths and 401 recovery do not bypass or retain stale copies of that authority.

Open symptoms include #17265 and #27165. Direct-keyring concurrency regression coverage is tracked by #33540. Codex-owned one-shot 401 recovery and session rebuild remain proposed in #30294.

3. Classify refresh failures by remediation

The current refresh path receives RMCP AuthError::TokenRefreshFailed(String) for both definitive rejection and transient token-endpoint failures, and currently converts the entire variant to AuthorizationRequired.

The lifecycle should distinguish at least:

  • invalid_grant: the refresh token/grant is invalid, expired, or revoked; require a new authorization grant.
  • invalid_client: recover or re-register the OAuth client when possible before asking for a new user grant; see #29630.
  • network failures, timeouts, HTTP 5xx, and provider-declared temporary failures: preserve the durable credential authority, report a retryable failure, and do not immediately open OAuth.
  • persistence failure after the provider may have rotated the token: treat the outcome as unknown, fail closed, reread the authoritative store, and avoid a blind replay or silently switching credential stores.

Earlier PRs #26519 and #26237 described part of this distinction but closed without merge.

Relevant OAuth semantics:

4. Recognize authentication-required errors through every wrapper

Authentication-required state must survive all RMCP transport/error wrappers and reach clients as a typed reason.

The current nested classifier recognizes StreamableHttpError::Auth(AuthError::AuthorizationRequired | TokenExpired) but not StreamableHttpError::AuthRequired(...).

Related merged work: #29877 and #30257.

5. Provide an actionable reauthorization flow

When a refresh is definitively rejected:

  • an interactive surface should offer or open OAuth, wait for the callback, and retry startup at most once;
  • cancellation or failed login must not create a browser/retry loop;
  • a headless or automation surface should return a stable typed result and a reconnect action without depending on a browser.

Related issues: #13956 and #29630. Related open work: #30294.

6. Reconcile credentials without requiring an app restart

The expected timing differs by login path:

  • For an OAuth callback owned by the running app-server, invalidate and reconcile the affected runtime immediately after the credential is persisted. This path is now covered by #35204.
  • For codex mcp login <server> running in another process, detect the new credential generation through store watching, IPC, or a reread/reconciliation boundary no later than the next operation or turn.

In both cases, users should not need to restart Codex or create a new conversation. The external CLI-login adoption path remains open; see #14144.

7. Keep one authoritative credential lifecycle across surfaces

CLI, Desktop, IDE, and automations using the same Codex home should not overwrite rotated credentials, resurrect logged-out credentials, or retain stale generations independently.

#32229 substantially addresses concurrent refresh. Login and logout still need to participate in the same per-credential transaction boundary; that work is proposed in #30295.

Credential durability and store selection also need actionable diagnostics. Related reports include #15122 and the volatile Linux keyring case #34943.

8. Expose actionable lifecycle status

Status APIs and UIs should distinguish:

  • no authentication required;
  • first login required;
  • refreshable credential available;
  • reauthentication required;
  • retryable provider/network failure;
  • ambiguous refresh/persistence outcome;
  • policy-disabled;
  • configured in runtime but unavailable to management commands.

Upstream now exposes unknown when OAuth support cannot be determined (#36045); the broader remediation-specific status model is still missing.

Expected state machine

For each OAuth-backed MCP:

  1. Valid access token: initialize normally.
  2. Expired or near-expiry access token plus a usable refresh token: run the serialized refresh transaction and persist any rotation before exposure.
  3. Another process already committed a newer generation: adopt the authoritative winner without contacting the provider again.
  4. Transient provider/network failure: preserve credentials and report a retryable failure without requesting a new grant.
  5. Provider refresh succeeded but persistence outcome is uncertain: fail closed, reconcile against the authoritative store, and do not blindly replay the previous rotating token.
  6. invalid_grant: emit typed reauthentication-required state.
  7. invalid_client: attempt the appropriate client-registration recovery or return a distinct remediation.
  8. Interactive surface: offer/launch OAuth and retry startup at most once.
  9. Successful in-process callback: reconcile the affected runtime immediately.
  10. Successful external CLI login: loaded runtimes adopt the new generation by the next operation or turn, without restart.
  11. Headless surface: fail deterministically with a stable reconnect action and no browser dependency.

Acceptance criteria

  • The proactive refresh behavior landed in #32229 works consistently for startup, tool calls, routed/plugin servers, and long-lived threads.
  • A valid stored refresh token survives access-token expiry without user action.
  • invalid_grant produces an actionable reconnect flow; timeout or HTTP 5xx does not delete credentials or immediately request a new grant.
  • Two processes cannot replay or overwrite a rotating refresh token.
  • Login, refresh, and logout share the same per-credential transaction boundary.
  • A successful app-owned OAuth callback repairs the current thread without restart.
  • A successful external CLI login is adopted by loaded runtimes no later than their next MCP operation or turn.
  • A plugin MCP is not disabled merely because managed plugin requirements exist without an MCP allowlist. Landed in #35280.
  • Runtime and codex mcp list|get|login resolve the same effective plugin MCP registry.
  • Headless clients receive stable typed remediation rather than a browser dependency or silently missing tools.
  • Status surfaces expose the actual lifecycle/policy failure reason, including inconclusive discovery.

Operational follow-ups

These are useful, but should not be treated as substitutes for automatic lifecycle recovery.

Multi-server repair

Provide a sequential repair flow such as codex mcp login --all-required or codex mcp repair. It should operate only on servers that need first login or reauthorization, skip healthy refreshable credentials and unsupported/static auth, and never open several browser flows concurrently.

Failed-startup cleanup

If initialize or initial list_tools fails, explicitly shut down the client/transport when ownership is not already guaranteed to do so. This should have its own focused regression or resource-leak reproduction rather than being assumed to be an OAuth root cause.

Non-goals

  • Per-session corporate approval or step-up authorization is distinct from OAuth token refresh and should retain its own policy/consent semantics.
  • The request is not to open a browser for every MCP at startup.
  • Static bearer-token and stdio authentication are not OAuth refresh flows.
  • This issue does not require Codex to copy Claude Code's internal implementation.

Why keep an umbrella issue

The linked issues and PRs correctly address individual layers, but enterprise users experience the combined failure as one outcome: an MCP that was previously authorized stops being available and does not recover predictably.

A lifecycle owner and an end-to-end regression suite covering CLI, app-server, plugin registries, concurrent processes, rotating refresh tokens, transient provider failures, and active-runtime reconciliation would ensure the landed pieces compose into a dependable corporate workflow.

Activity

  1. added
    enhancementNew feature or request
    mcpIssues related to the use of model context protocol (MCP) servers
    authIssues related to authentication and accounts
    on Jul 23, 2026
  2. publi0 commented on Jul 27, 2026

    @publi0
    Author

    Upstream update — rechecked against main@95637f705 on July 27, 2026

    Since this issue was opened, the following pieces have landed:

    This means the first plugin-filtering gap and the app-owned OAuth-callback part of runtime reconciliation are addressed. Registry parity for standalone codex mcp login (#34859), external-process credential adoption, lifecycle diagnostics, and volatile credential durability remain open.

    Refresh-token behavior still missing on current main

    1. Refresh is expiry-driven only. A provider can reject an access token with 401 while Codex still considers its local expires_at valid (revocation, provider-side invalidation, clock/metadata mismatch). In that case the current path does not force a refresh, rebuild the MCP transport/session, and retry once.
    2. AuthError::TokenRefreshFailed(String) currently collapses definitive OAuth rejection and transient token-endpoint failures. Mapping the whole variant to AuthorizationRequired conflates invalid_grant with temporarily_unavailable, HTTP 5xx, timeouts, and network failures, which can turn a temporary outage into a full browser authorization flow.
    3. The nested startup classifier recognizes StreamableHttpError::Auth(AuthorizationRequired | TokenExpired) but not StreamableHttpError::AuthRequired(...).
    4. Route MCP OAuth recovery through Codex #30294 (Codex-owned transport recovery) and Serialize MCP OAuth login and logout #30295 (serialized login/logout) were closed unmerged on July 26. I found no equivalent replacement for those behaviors in current main.

    Locally validated recovery semantics

    A focused implementation and regression suite validates:

    • startup 401 with an unexpired persisted access token → force serialized refresh, rebuild transport, retry initialize once;
    • active-session tools/list 401 → force refresh, rebuild the MCP session, retry the operation once;
    • concurrent/newer credential generation → reread under the shared lock and adopt the newer durable credentials instead of replaying a stale rotating refresh token;
    • token endpoint invalid_grant → typed reauthorization required;
    • token endpoint 503 temporarily_unavailable → preserve durable credentials, return a retryable failure, and do not launch OAuth;
    • interactive reauthorization only after definitive rejection, with at most one login/startup retry.

    Targeted validation passed: 5/5 Streamable HTTP OAuth startup/session integration tests, 8/8 refresh-transaction tests, and 122/122 codex-mcp tests.

    These results narrow lifecycle gaps 2–5: recovery needs both an expiry boundary and a one-shot authentication-failure boundary, while browser reauthorization must remain reserved for definitive grant rejection. A typed RMCP refresh error would be preferable to parsing TokenRefreshFailed(String).

  3. publi0 commented on Jul 27, 2026

    @publi0
    Author

    Additional confirmed gap: background SSE refresh bypasses durable persistence

    A concrete reproduction now confirms one of the remaining lifecycle gaps described in this issue.

    Even with the recently landed serialized refresh transaction, RMCP can autonomously refresh an expired access token during an SSE get_stream reconnect. That refresh happens inside RMCP's AuthClient, outside Codex's guarded OAuthPersistor transaction.

    With a provider that rotates refresh tokens on every use, the result is:

    durable store: access A0 / refresh R0
    RMCP memory:   access A1 / refresh R1
    provider:      R0 invalidated after issuing R1
    

    The current process continues using the refreshed in-memory credentials, but a restarted or parallel Codex process reloads R0 and receives:

    invalid_grant: The refresh token was already used.
    

    This means the landed concurrency fixes protect Codex-initiated refreshes, but not refreshes initiated autonomously by RMCP's background Streamable HTTP/SSE path.

    Detailed timeline, source-level analysis, regression test, and locally validated fix:

    The end-to-end acceptance criterion should therefore explicitly cover background get_stream/SSE reconnects: every token lookup or refresh must use the same serialized credential transaction, and rotated credentials must be durably persisted before the refreshed access token is exposed to the transport.

  4. publi0 commented on Jul 28, 2026

    @publi0
    Author

    Upstream update — rechecked against main@9ea975a2d on July 28, 2026

    The latest upstream round resolves one of the concrete lifecycle gaps documented above:

    • Upgrade rmcp to 3.0.0-beta.3 #35720 upgrades RMCP to 3.0.0-beta.3 and introduces a typed distinction between AuthError::TokenRefreshRejected and transient TokenRefreshFailed errors. Codex now maps only definitive refresh-token rejection to reauthorization. The local string parser for invalid_grant is no longer necessary, and the upstream regression test confirms that a temporary provider failure preserves credentials without requesting a new grant.
    • Route MCP OAuth through configured HTTP clients #35806 routes MCP OAuth through the configured HTTP client path, including the authorization manager used for token refresh. This is important for enterprise proxy/VPN environments and complements the earlier routed discovery work.

    This means lifecycle gap 3 ("Classify refresh failures by remediation") is now substantially addressed for invalid_grant versus transient refresh failures.

    Gaps still present on current main

    1. Background Streamable HTTP/SSE refresh can still bypass durable persistence. The OAuth transport still uses RMCP's AuthClient directly. A refresh initiated autonomously during get_stream can rotate credentials in memory without committing the new refresh token through Codex's serialized OAuthPersistor transaction. The detailed reproduction remains in Codex does not auto-refresh routed MCP OAuth tokens even when a refresh token is stored #17265, and the same behavior has also been independently reproduced after the RMCP 3 upgrade: Codex does not auto-refresh routed MCP OAuth tokens even when a refresh token is stored #17265 (comment)
    2. There is still no Codex-owned one-shot recovery after an authenticated operation receives 401 while local expires_at remains valid. Current main does not force a refresh, rebuild the MCP session, and retry the operation once.
    3. Authentication-required classification remains incomplete. The startup classifier recognizes StreamableHttpError::Auth(AuthorizationRequired | TokenExpired) but still misses StreamableHttpError::AuthRequired(...).
    4. External login adoption and lifecycle serialization remain open. Already-running processes still lack a reliable boundary for adopting credentials written by an external codex mcp login; login/logout do not yet participate in the same refresh transaction; and standalone plugin registry parity remains tracked by Remote-installed plugin MCP prompts codex mcp login, but the command cannot resolve the server #34859.
    5. Failed-startup cleanup and actionable status remain open. Initial initialize/list_tools failures still do not explicitly shut down the owned client in that path, and lifecycle states are not yet exposed with the remediation detail described in this issue.

    The recent cached-tool, optional-startup-grace, and concurrent-catalog work (#35590, #35742, and #35777) should materially reduce MCP startup and inventory latency. Those are useful operational improvements, but they do not replace the remaining OAuth recovery semantics above.

    Rebased local validation

    The focused implementation was rebased onto main@9ea975a2d, preserving the new RMCP 3 behavior and the configured HTTP-client routing. The now-redundant invalid_grant parser and duplicate transient-failure test were removed. Only the still-missing behaviors were retained:

    • persistent refresh/persistence before Streamable HTTP operations, including SSE reconnects;
    • one-shot refresh, session rebuild, and retry after authentication failure;
    • complete AuthRequired classification;
    • failed-startup transport cleanup;
    • structured refresh diagnostics.

    Targeted validation after the rebase passed:

    • codex-rmcp-client: 204/204 tests;
    • codex-mcp: 133/133 tests;
    • MCP-specific TUI startup and /mcp status coverage: 28/28 tests.

    So the umbrella status is now narrower: typed refresh-failure classification and routed OAuth HTTP behavior have landed upstream, while background refresh durability, authentication-failure recovery, external credential adoption, and end-to-end lifecycle status still need upstream coverage.

  5. publi0 commented on Jul 29, 2026

    @publi0
    Author

    Consolidated upstream status — 0.146.0 and main@9a6668f67 (July 29, 2026)

    I rechecked the release tags and current source after the latest MCP changes. The lifecycle status is now narrower, but release availability matters: stable 0.146.0 was cut at 95637f705, while the newer fixes below first appear in 0.146.0-alpha.15, alpha.16, or only on current main.

    Fixed and shipped in stable 0.146.0

    These changes address plugin projection, app-owned callback reconciliation, proxy-aware discovery, and the original concurrent Codex-owned refresh/token-clobbering path.

    Fixed in 0.146.0-alpha.15 or newer, but not in stable 0.146.0

    Fixed on current main, not yet present in alpha.16

    Remaining lifecycle gaps

    1. Background Streamable HTTP/SSE refresh durability is still open. Current main still backs the OAuth transport with RMCP's AuthClient. An autonomous refresh during get_stream/SSE reconnect can bypass the Codex-owned persistence transaction. There is still no upstream equivalent of routing every transport token lookup through OAuthPersistor before the request. The concrete reproduction and regression test remain documented in Codex does not auto-refresh routed MCP OAuth tokens even when a refresh token is stored #17265:
      Codex does not auto-refresh routed MCP OAuth tokens even when a refresh token is stored #17265 (comment)
    2. One-shot recovery after an unexpected authenticated-operation 401 is still open. If local expires_at is still valid, current upstream does not force refresh, rebuild the MCP session, and retry once.
    3. Authentication-required classification is not yet complete across every RMCP wrapper.
    4. External credential adoption remains incomplete. A running process still lacks a guaranteed boundary for adopting credentials written by a separate codex mcp login.
    5. Login/logout transaction parity and standalone plugin registry parity remain open. The latter is tracked by Remote-installed plugin MCP prompts codex mcp login, but the command cannot resolve the server #34859.
    6. Actionable lifecycle status and focused failed-startup cleanup remain open.

    The important clarification is that the primary serialized refresh transaction is shipped and correct for Codex-owned expiry refreshes, but it does not yet close the separately reproduced RMCP background/SSE bypass or the unexpected-401 recovery path.

  6. publi0 commented on Aug 1, 2026

    @publi0
    Author

    Status update — 2026-07-31

    The following lifecycle behaviors are now covered by upstream:

    • Concurrent aggregate-store updates no longer lose credential changes: #30292.
    • Refresh uses a serialized read-refresh-write transaction, rereads the authoritative store, and adopts credentials refreshed by another process: #32229.
    • A refresh response that omits refresh_token preserves the existing refresh token; a rotated token is persisted before use: #32229.
    • Proactive refresh, runtime reconciliation, connection replacement, and in-process OAuth callback invalidation are covered by #34930, #34952, #34957, #35146, #35151, and #35204.
    • OAuth discovery now uses the configured HTTP client: #35239.
    • Missing managed-plugin MCP allowlists no longer disable all plugin MCPs: #35280.
    • unknown now distinguishes inconclusive OAuth discovery from confirmed unsupported auth: #36045.
    • Host/executor credential isolation and per-thread MCP runtime binding are covered by #36306, #36310, and #36355.

    The following gaps remain open:

    • Classify invalid_grant, invalid_client, transient network/timeout/5xx failures, and persistence ambiguity differently.
    • Rebuild the MCP session/transport and retry once after an unexpected 401.
    • Provide a bounded interactive reauthorization flow without browser loops, plus a stable headless remediation.
    • Adopt credentials written by an external codex mcp login process in already-loaded runtimes without restart.
    • Serialize login/logout with the same credential transaction used by refresh across CLI, Desktop, IDE, and automation surfaces.
    • Keep runtime and codex mcp list|get|login on the same effective plugin registry.
    • Expose remediation-specific lifecycle states beyond unknown, and add sequential multi-server repair.
    • Guarantee transport cleanup after failed initialize or initial list_tools.

    The omitted-refresh-token and basic concurrent-refresh loss scenarios are therefore considered addressed; the remaining work is failure classification, session recovery, cross-process adoption, and end-to-end lifecycle integration.

  7. zarcht commented on Aug 3, 2026

    @zarcht

    have raised this to customer support (case ID: 12091045) with two feedback ID sent.
    019f9d83-9ac1-72d2-8cf4-13270717f233
    019f9805-6070-77f2-b639-ef9030bd4d59

    Please prioritize a permanent and thorough fix for this as MCP is a fundamental function.

  8. mcarra34 commented on Aug 19, 2026

    @mcarra34

    Additional report — Stripe app connection in Codex Desktop

    I authenticated/reconnected the Stripe app through the app UI, but the Stripe account-info call still fails immediately with:

    UNAUTHORIZED
    oauth_token_invalid_grant
    Reauthentication required
    

    The in-app reconnect flow completed, but did not repair the connection. No Stripe account or token details are included here.

    This appears consistent with a stale/invalid OAuth grant surviving a successful reconnect in the desktop/plugin path.

  9. daniel-meismer-zocdoc commented on Aug 20, 2026

    @daniel-meismer-zocdoc

    Additional ChatGPT/Codex Desktop regression datapoint on macOS:

    • Last night, bundled Codex 0.148.0-alpha.15 had an expired MCP access token and attempted to refresh it with an expired/
      invalid refresh token. The MCP OAuth token endpoint returned 400 invalid_grant, and the UI displayed a banner to
      reconnect the MCP.

    • Today, after updating to 0.148.0-alpha.21, the same situation produces no reconnect banner and offers no way to repair
      the connection.

    Expected: 400 invalid_grant during refresh should mark the MCP as requiring reauthentication and display the reconnect
    flow.

  10. petervledder commented on Sep 16, 2026

    @petervledder

    Additional Windows / Codex Desktop reproduction for the external-CLI-login reconciliation gap.

    Environment:

    • Windows desktop
    • Codex CLI 0.129.0
    • Remote Streamable HTTP MCP server configured as flow_mcp with OAuth

    Reproduction:

    1. An existing Codex Desktop task tries to initialize/list tools from flow_mcp.
    2. Startup fails with: failed to refresh OAuth tokens for server flow_mcp: OAuth refresh token was rejected: Server returned error response: invalid_grant: Token is not active: OAuth authorization required.
    3. In a separate terminal, run codex mcp login flow_mcp and complete the browser authorization. The CLI reports: Successfully logged in to MCP server 'flow_mcp'.
    4. Retry the MCP operation in the already-running Desktop task.

    Actual result:
    The already-loaded task runtime still fails with the same stale-refresh-token invalid_grant error. It does not adopt the successful external CLI login on the next MCP operation.

    Expected result:
    After a successful codex mcp login <server>, loaded Desktop/task runtimes should reconcile the credential generation before their next MCP operation, or surface an explicit reconnect action. This was reproduced without exposing tokens, authorization URLs, client IDs, or user data.

  11. m13v commented on Sep 16, 2026

    @m13v

    the 'loaded runtimes reread the store by the next operation' path for external CLI login is the part that bites later. that reread rides the same serialized store lock the #32229 refresh transaction holds, so on the volatile keyring you flag in #34943 you've turned 'adopt the new login next turn' into a per-turn stall across every healthy MCP, not just the one that re-logged in. the adoption trigger you'd actually want fires off a generation counter you can check without taking the write lock.

  12. jcoggon commented on Sep 29, 2026

    @jcoggon

    Reproducible refresh-response-loss recovery boundary in bundled app-server

    Suggested destination: #35006

    Tested macOS Desktop bundle 26.924.22138 / build 11645, bundled CLI/app-server
    0.158.0-alpha.2.1. This used an isolated temporary CODEX_HOME, explicit file-backed
    MCP OAuth storage, synthetic credentials, and a disposable loopback OAuth/MCP
    fixture. No real account, active Desktop profile, or Keychain entry was used.

    The fixture rotates refresh tokens and returns the identical successor pair for
    a bounded predecessor retry while that successor is unused. For accelerated
    testing, access tokens last 35 seconds and the predecessor retry window is five
    seconds. The fixture exposes MCP initialization and an empty tools/list; this is
    not an authenticated tool-execution test or a complete OAuth conformance server.

    Observed results:

    • Normal proactive refresh and fresh app-server processes reuse persisted
      credentials successfully. Two concurrent processes at a refresh threshold
      produce exactly one new provider rotation and authorized MCP discovery.
    • When the fixture commits a rotation and destroys its HTTP response, no
      authorized MCP discovery follows that attempt.
    • A subsequent process after the five-second recovery window sends the
      predecessor, receives invalid_grant, and reports authStatus=notLoggedIn with
      toolsError=true.
    • In a separate attempt, a new process started inside the retry window recovers
      the same pair and completes authorized MCP discovery. Provider counters show
      one retry recovery and no additional rotation or rejected replay.

    This demonstrates a recovery boundary after an ambiguous transport outcome. It
    does not establish a concurrent-store bug, the behavior of the Desktop routed
    or direct-Keychain path, or the cause of any historical production incident.
    The five-second fixture window also does not establish behavior with a provider's
    longer production grace period.

    Could the refresh transaction support a bounded immediate retry for this
    ambiguous outcome under the existing credential lock, with an appropriate
    provider-compatible recovery policy? Blind retries are not generally safe when
    a provider has no predecessor grace period. Definitive OAuth rejection must
    remain distinct from transport ambiguity, and persistence failures must remain
    fail-closed. Server replay protection should not be relaxed.

    Sanitized fixture and app-server probe source are available for review; they
    contain no real endpoints or credentials.

  13. m13v commented on Sep 29, 2026

    @m13v

    bounded predecessor retry has a cost past the no-grace case: with reuse detection, a retry landing outside the window isn't a plain invalid_grant, it revokes the whole token family, so every other surface on that grant gets logged out too.

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

    authIssues related to authentication and accountsenhancementNew feature or requestmcpIssues related to the use of model context protocol (MCP) servers

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions