Skip to content

fix(auth): keep MCP agent logins alive and let users disconnect agents - #361

Merged
vishr merged 1 commit into
mainfrom
fix/long-lived-mcp-tokens
Oct 20, 2026
Merged

vishr merged 1 commit into
mainfrom
fix/long-lived-mcp-tokens

Conversation

@vishr

@vishr vishr commented Oct 20, 2026

Copy link
Copy Markdown
Member

Problem

Codex runs many processes off one stored MCP credential. Each one refreshes from its own in-memory copy when it next needs Fanout, which can be minutes or hours after another process has already rotated the refresh token.

The one-minute reuse grace from #359 did not cover that. On prod, one Codex process rotated the token at 03:54:05Z, and a second presented the rotated token at 03:56:25Z. The family was revoked and every Codex process was signed out. With one-hour access tokens, this happened about hourly.

Other MCP server operators report the same pattern upstream: openai/codex#46028, #45944 and #48507. Codex's own fix (mcp_oauth_refresh_coordination) is still labelled under development.

Change

Token lifetimes:

Before After
Access token 1 h 30 d
Refresh token 30 d, renewed on use 90 d, renewed on use
Same-client reuse of a rotated refresh token 60 s one access-token lifetime (30 d), while the family is live

Rotation and replay-family revocation stay for reuse after the window, for expired tokens, other clients and revoked families.

Connected agents:

  • Settings lists the agents connected to the signed-in user's account (GET /api/auth/oauth/connections).
  • Disconnect revokes all of one client's tokens for that user at once (POST /api/auth/oauth/connections/{id}/revoke), with an oauth_connection.revoked audit event.
  • Both routes need a browser session and the CSRF header. They are scoped to the caller's own account. Another user's client returns 404.

Pending authorization codes: a disconnect, and the existing administrator revoke-all (POST /api/users/{id}/access/revoke), now delete that client's or user's pending authorization codes. A code issued before the disconnect can no longer mint a new family.

Security trade-off

  • Longer access tokens: a leaked access token stays valid until it is revoked, for up to 30 days instead of one hour. Every request checks the database, so Disconnect, revoke-all and suspension take effect immediately.
  • Weaker replay detection: a stolen refresh token used within 30 days of rotation no longer triggers detection. Whoever can read the stored credential already holds an access token with the same lifetime, which limits how much this widens exposure.
  • Accepted race: if a Disconnect lands between a code being consumed and its tokens being issued (milliseconds), the agent's new connection appears in the list and can be disconnected again.

Verification

  • just test ./internal/auth/... ./internal/api/... ./cmd/...
  • just ui-check, just docs-generate-check, Settings unit tests
  • Codex adversarial review. Its one finding, pending codes surviving a disconnect, is fixed and covered by tests.

https://claude.ai/code/session_018HfyEXHyGD1utmKF4CQtyQ

Codex runs many processes off one stored MCP credential, and each refreshes
from its own in-memory copy when it next needs Fanout, minutes or hours
after another process has already rotated the refresh token. A one-minute
reuse grace did not cover that: on prod a second process presented the
rotated token 140 seconds later, the family was revoked, and every Codex
process was signed out, about once an hour. Other MCP operators report the
same pattern upstream (openai/codex#46028, #45944, #48507).

Access tokens now last 30 days and refresh tokens 90 days, renewed on each
use, so agents refresh at most monthly and re-login only after 90 idle days.
A rotated refresh token stays reusable by its own client, while its family
is live, for one access-token lifetime. Rotation and replay-family
revocation remain for later reuse.

Access tokens are checked against the database on every request, so the
longer lifetime is safe only if users can cut off one agent. Settings now
lists the agents connected to the signed-in user's account, through
GET /api/auth/oauth/connections, and Disconnect revokes one client's tokens
through POST /api/auth/oauth/connections/{id}/revoke, with an audit event.
Disconnecting, and an administrator's revoke-all, also delete that
client's or user's pending authorization codes, so a code issued before
the disconnect cannot mint a new family.

Claude-Session: https://claude.ai/code/session_018HfyEXHyGD1utmKF4CQtyQ
@vishr
vishr merged commit 23a4fed into main Oct 20, 2026
17 checks passed
@vishr
vishr deleted the fix/long-lived-mcp-tokens branch October 20, 2026 08:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant