Skip to content

fix(auth): recover expired dynamic client registrations - #3260

Draft
daleselaji-dev wants to merge 4 commits into
modelcontextprotocol:mainfrom
daleselaji-dev:agent/recover-expired-client-registration
Draft

fix(auth): recover expired dynamic client registrations#3260
daleselaji-dev wants to merge 4 commits into
modelcontextprotocol:mainfrom
daleselaji-dev:agent/recover-expired-client-registration

Conversation

@daleselaji-dev

Copy link
Copy Markdown

Problem

An OAuth client can remain permanently stuck after a dynamically registered client secret expires. The stored registration is reused, and token endpoint invalid_client responses are treated as ordinary failures, so re-authorization retries with the same dead credentials.

Root Cause

OAuthClientProvider loaded client_secret_expires_at but never used it. The token storage protocol also had no way to clear the persisted client registration or tokens when the authorization server rejected the client credentials.

Solution

Treat a non-zero, past client_secret_expires_at as an unusable stored registration and clear it before the next flow. Also detect invalid_client from the response body regardless of whether the authorization server uses HTTP 400 or 401, clear the bound credentials, and allow the next flow to register again.

Changes

  • Allow TokenStorage setters to receive None to clear stored tokens or client information.
  • Clear expired registrations during provider initialization.
  • Clear persisted registration and tokens on invalid_client during token exchange or refresh.
  • Add regression coverage for proactive expiry, authorization-code exchange failures, and refresh failures.

Testing

  • py -m pytest tests/client/test_auth.py tests/client/test_scope_bug_1630.py tests/client/auth/extensions/test_client_credentials.py tests/client/auth/extensions/test_identity_assertion.py -q — 172 passed, 1 xfailed.
  • py -m ruff format --check src/mcp/client/auth/oauth2.py tests/client/test_auth.py tests/client/test_scope_bug_1630.py tests/client/auth/extensions/test_client_credentials.py tests/client/auth/extensions/test_identity_assertion.py — passed.
  • py -m ruff check src/mcp/client/auth/oauth2.py tests/client/test_auth.py tests/client/test_scope_bug_1630.py tests/client/auth/extensions/test_client_credentials.py tests/client/auth/extensions/test_identity_assertion.py — passed.
  • git diff --check — passed.

Compatibility/Risk

This changes the TokenStorage setter contract so storage implementations must treat None as deletion. The runtime behavior is limited to expired or server-rejected registrations; valid registrations and non-invalid_client failures retain their existing flow.

Notes for Reviewer

Pyright is unverified locally because the incremental worktree has no repository .venv. An alternate environment had mismatched mcp_types, httpx2, and project dependency versions and reported broad pre-existing import/type diagnostics; those results are not presented as a passing typecheck.

Linked Issue

Closes #3256

@iamroylim iamroylim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One compatibility concern beyond the documented risk: this widens a structural TokenStorage protocol and immediately relies on the new runtime behavior. I reproduced it at 6a71a0296193b7ea0c617fa8742cd4f9b2c54cf6 with a storage implementation that follows the pre-change setters and rejects None: _initialize() sees an expired registration, reaches _clear_stored_credentials(), then raises from set_client_info(None) before set_tokens(None) runs. That means an otherwise conforming third-party storage can fail during startup after upgrading and never reach re-registration.

Could we preserve the old setters and introduce explicit deletion methods (or an optional deletion capability with a compatibility fallback)? If this is intentionally a breaking API change, I think it needs an explicit migration path plus a regression test using a legacy storage so the startup failure is an acknowledged release decision rather than an incidental one.

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.

OAuth client can never recover from an expired DCR client secret — even though the SDK's own server issues and enforces one

2 participants