Skip to content

Add direct-keyring regression coverage for concurrent MCP OAuth refreshes #33540

Open

Description

@runminglu

What version of Codex CLI is running?

0.144.4 when observed; 0.145.0-alpha.16 now installed (contains #32229)

What subscription do you have?

Pro; not relevant to MCP OAuth credential persistence

Which model were you using?

N/A — MCP startup and OAuth credential refresh

What platform is your computer?

Darwin 25.4.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

Terminal.app and ChatGPT desktop app (concurrent Codex processes)

Codex doctor report

Not included: the current installed build already contains #32229; this report documents an observed 0.144.4 race and proposes missing direct-keyring regression coverage.

What issue are you seeing?

Two concurrent Codex processes shared an OAuth MCP credential through macOS Keychain. On Codex 0.144.4, the standalone client successfully refreshed a rotating token, but the Keychain entry remained on the predecessor. A later app-server process loaded that stale entry and the provider rejected its refresh with invalid_grant, making the MCP server appear to lose authorization.

Observed sequence:

  • The standalone process refreshed the credential successfully.
  • No successful Keychain save followed that refresh, and the durable entry remained stale.
  • A second Codex process later loaded the stale Keychain credential.
  • Its refresh attempted to reuse the rotated predecessor and was rejected.

#32229 appears to address the production root cause by serializing each credential's read-refresh-write transaction, rereading the authoritative store after locking, persisting before exposure, and letting later processes adopt the winner. Its successful concurrency regression currently exercises the JSON file store; the equivalent successful direct-keyring path is not covered end to end.

What steps can reproduce the bug?

  1. Configure a remote OAuth MCP server that rotates refresh tokens and use the direct keyring credential backend.
  2. Start two Codex processes that share the same CODEX_HOME and macOS Keychain entry (for example, standalone CLI plus app-server).
  3. Let the stored access token expire so both clients need a proactive refresh.
  4. Allow the first process to refresh, then let the second process refresh from the shared durable credential.
  5. Without serialized durable persistence, the second process can replay the rotated predecessor and receive invalid_grant.

The proposed deterministic regression forces both clients through the real per-credential lock contention path, expects exactly one provider refresh, and verifies the second client adopts the direct-keyring winner.

What is the expected behavior?

The first process should persist the refreshed credential to the lifecycle-pinned Keychain store before exposing it. The second process should acquire the credential lock, reread Keychain, observe the unexpired winner, and adopt it without contacting the provider. No fallback file should be created.

Additional information

I prepared a focused test-only commit and would like an invitation to submit it under the repository's external-contribution policy.

I searched the existing MCP OAuth/keyring issues and did not find one covering this exact successful direct-keyring concurrency regression gap.

Activity

  1. added
    enhancementNew feature or request
    CLIIssues related to the Codex CLI
    mcpIssues related to the use of model context protocol (MCP) servers
    authIssues related to authentication and accounts
    on Jul 16, 2026
  2. ttys3 commented on Aug 26, 2026

    @ttys3

    I observed a closely matching failure on Linux with Codex CLI 0.149.1 and the direct Secret Service keyring backend. The corresponding 0.149.1 source tree I inspected includes #32229 (6962a2e), so this may be a remaining direct-keyring persistence gap after the serialized-refresh fix rather than only the pre-#32229 race originally reported here.

    Environment:

    • Codex CLI: 0.149.1 (downstream package)
    • Platform: Linux x86_64 with a Secret Service-compatible keyring
    • MCP transport/auth: Streamable HTTP + OAuth/DCR
    • Credential configuration: mcp_oauth_credentials_store = "auto", resolving to direct Secret Service storage
    • Provider behavior: one-hour access tokens and refresh-token rotation on every successful refresh
    • Reproduction: intermittent

    Sanitized sequence and evidence:

    1. codex mcp login <server> completed successfully. The Codex MCP Credentials Secret Service item contained both an access token and a refresh token.
    2. After the access token expired, provider-side durable metadata recorded another token-pair issuance for the same OAuth client. The timing is consistent with a successful refresh.
    3. The Secret Service item did not advance to the new generation: its stored expires_at value and item modification time both remained at the original login generation.
    4. A later Codex process successfully loaded that keyring item, then attempted refresh and received invalid_grant. Codex reported that OAuth reauthentication was required.
    5. The provider's durable state no longer contained that client's refresh-token family, which is consistent with replay revocation after the predecessor token was presented again.
    6. A fresh authorization restored operation. Immediate Codex restarts currently work while the new access token remains valid; I have not reproduced the failure deterministically yet.

    Relevant redacted Codex log lines:

    keyring.load success, service=Codex MCP Credentials, account=<redacted>
    MCP OAuth refresh token was rejected; reauthorization required
    invalid_grant: the refresh token is invalid or has expired
    

    This adds two data points to the original report:

    I cannot yet distinguish a failed/aborted Secret Service write from process exit, cancellation, or a lost response after the provider committed rotation. The evidence places the failure after provider-side token issuance and before successful durable replacement of the direct-keyring entry.

    This also appears related to the unresolved “provider refresh succeeded but persistence outcome is uncertain” state tracked in #35006. A Linux Secret Service end-to-end regression test, plus logging around the direct-keyring save/commit result, may help make this deterministic.

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

    CLIIssues related to the Codex CLIauthIssues 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