fix(auth): recover expired dynamic client registrations - #3260
fix(auth): recover expired dynamic client registrations#3260daleselaji-dev wants to merge 4 commits into
Conversation
iamroylim
left a comment
There was a problem hiding this comment.
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.
Problem
An OAuth client can remain permanently stuck after a dynamically registered client secret expires. The stored registration is reused, and token endpoint
invalid_clientresponses are treated as ordinary failures, so re-authorization retries with the same dead credentials.Root Cause
OAuthClientProviderloadedclient_secret_expires_atbut 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_atas an unusable stored registration and clear it before the next flow. Also detectinvalid_clientfrom 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
TokenStoragesetters to receiveNoneto clear stored tokens or client information.invalid_clientduring token exchange or refresh.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
TokenStoragesetter contract so storage implementations must treatNoneas deletion. The runtime behavior is limited to expired or server-rejected registrations; valid registrations and non-invalid_clientfailures retain their existing flow.Notes for Reviewer
Pyright is unverified locally because the incremental worktree has no repository
.venv. An alternate environment had mismatchedmcp_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