Skip to content

auth: reuse the dynamically registered client across authorizations - #1419

Merged
guglielmo-san merged 2 commits into
modelcontextprotocol:mainfrom
luiacuaniello:fix-reuse-dcr-registration
Oct 20, 2026
Merged

guglielmo-san merged 2 commits into
modelcontextprotocol:mainfrom
luiacuaniello:fix-reuse-dcr-registration

Conversation

@luiacuaniello

Copy link
Copy Markdown
Contributor

Fixes #1416.

AuthorizationCodeHandler.Authorize called handleRegistration every time it ran, and did not keep the registration response. With dynamic client registration, every authorization registered a new client: the initial 401, every step-up 403 with insufficient_scope, and every re-authorization after a refresh is rejected with invalid_grant. Each one left an orphaned client on the authorization server, which the SDK never deletes (it does not implement RFC 7592). It also showed the user a consent screen for a new client on each step-up, and the scopes accumulated in grantedScopes had been granted to a client that was no longer used.

This keeps the registered client on the handler, keyed by the authorization server issuer, and reuses it in handleRegistration. The TypeScript SDK also registers only when it has no stored client information. The client is registered again when:

  • its client_secret_expires_at has passed (RFC 7591, section 3.2.1), or
  • an authorization that used it fails, since the authorization server may have deleted it. This keeps the previous behavior, a new registration on the next attempt, for that case.

DynamicClientRegistrationConfig documents the reuse. There is no API change.

Tests:

  • TestAuthorize_ReusesDynamicRegistration: a step-up authorization uses the client_id registered by the first one. Fails without the change.
  • TestAuthorize_ForgetsDynamicRegistrationAfterFailure: after an authorization with a client the server no longer knows fails, the next one registers a new client.
  • TestHandleRegistration_StoredDynamicRegistration: a stored client is reused unless its client secret has expired.

The last two exercise the new code, so they cannot fail on main. I checked each one by removing the part of the change it covers: each then fails, and only that one.

This touches the same part of Authorize as #1104. The two are independent, but whichever lands second will need a small rebase.

The client conformance suite shows the change too. On main, auth/scope-step-up and auth/scope-retry-limit each record a second client-registration when the step-up happens. With this change they record one, so the total goes from 239 to 237 passed checks, with no failures.

go test ./..., go test -race on auth and mcp, go vet ./..., staticcheck v0.6.1 and scripts/client-conformance.sh are clean.

AI assistance: prepared with Claude Code; I reviewed and verified the change.

🤖 Generated with Claude Code

luiacuaniello and others added 2 commits October 19, 2026 21:51
AuthorizationCodeHandler.Authorize called handleRegistration every time
it ran, and did not keep the registration response. With dynamic client
registration, every authorization registered a new client: the initial
401, every step-up 403 with insufficient_scope, and every
re-authorization after a refresh is rejected with invalid_grant. Each
one left an orphaned client on the authorization server, which the SDK
never deletes (it does not implement RFC 7592). It also showed the user
a consent screen for a new client on each step-up, and the scopes
accumulated in grantedScopes had been granted to a client that was no
longer used.

Keep the registered client on the handler, keyed by the authorization
server issuer, and reuse it in handleRegistration. The TypeScript SDK
also registers only when it has no stored client information. The
client is registered again when:
- its client_secret_expires_at has passed (RFC 7591, section 3.2.1), or
- an authorization that used it fails, since the authorization server
  may have deleted it. This keeps the previous behavior, a new
  registration on the next attempt, for that case.

Tests:
- TestAuthorize_ReusesDynamicRegistration: a step-up authorization uses
  the client_id registered by the first one. Fails without the change.
- TestAuthorize_ForgetsDynamicRegistrationAfterFailure: after an
  authorization with a client the server no longer knows fails, the
  next one registers a new client.
- TestHandleRegistration_StoredDynamicRegistration: a stored client is
  reused unless its client secret has expired.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@guglielmo-san
guglielmo-san merged commit ed357b7 into modelcontextprotocol:main Oct 20, 2026
9 checks passed
@luiacuaniello
luiacuaniello deleted the fix-reuse-dcr-registration branch October 20, 2026 09:52
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.

auth: AuthorizationCodeHandler registers a new DCR client on every Authorize call

2 participants