fix(register): make client_id TTL configurable, default 7d#28
Merged
Conversation
The sealed client_id minted by POST /register previously used
a hardcoded 24h TTL. Refresh tokens last 7 days, so any MCP
client running longer than 24h failed its first refresh-token
rotation with `invalid_client: client registration expired`,
even with a still-valid refresh token. Most MCP clients
(codex_rmcp_client, Claude Code, Cursor, …) treat DCR as
one-shot at startup and surface the error to the user
instead of re-registering.
Fix:
- Default TTL bumped to 7 days, matching refreshTokenTTL so
the sealed client_id always covers a full refresh-token
cycle.
- New CLIENT_REGISTRATION_TTL env var lets operators raise
or lower the envelope. Go duration syntax; capped at 90d
at startup (wider windows extend the residual-reuse threat
of an exfiltrated client_id with no operational upside).
- Register handler signature gains a clientTTL parameter.
All call sites (main.go + tests) pass either the wired
cfg.ClientRegistrationTTL or DefaultClientTTL.
- Regression test (TestTokenRefresh_ClientLifecycleAroundTTL)
covers three boundary points: 48h-old (the bug repro),
6d23h-old (just inside the 7d envelope), and 1m past
expiry (must reject).
- 5 config tests for the env var: default, custom, non-
positive reject, above-cap reject, bad-format reject (the
"7d" case — time.ParseDuration doesn't accept the d
suffix; documented).
Rolling-deploy note: the TTL is sealed into each client_id at
registration time. Bumping the env var only affects newly-
issued client_ids; existing registrations stay on whatever
TTL was in effect when they were minted. Documented in the
new docs/runbooks/client-registration-expired.md.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The sealed
client_idminted byPOST /registerpreviously used a hardcoded 24h TTL. Refresh tokens last 7 days, so any MCP client running longer than 24h failed its first refresh-token rotation with:```
OAuth token refresh failed: Server returned error response:
invalid_client: client registration expired
```
even with a still-valid refresh token. Most MCP clients (codex_rmcp_client, Claude Code, Cursor, …) treat DCR as one-shot at startup, so they surface the error to the user instead of re-registering.
Fix
Tests
Rolling-deploy note
The TTL is sealed into each `client_id` at registration time. Bumping the env var only affects newly-issued `client_id`s; existing registrations stay on whatever TTL was in effect when they were minted. Currently-affected MCP clients have to re-register once (most clients trigger DCR on a fresh connection — a client restart suffices).
Documented in the new `docs/runbooks/client-registration-expired.md`, with operator playbook + the explicit "do NOT" list (no infinite TTL, no in-place extension).
Security trade-off
A longer `client_id` lifetime means a longer window for an exfiltrated `client_id` to be reused. Two mitigations bound that:
A stateful "auto-extend on use" pattern (so an active client never expires while idle ones still age out) is a meaningful design change, not just a TTL bump — kept on the local backlog for later.
Test plan