-
Notifications
You must be signed in to change notification settings - Fork 52.8k
fix(mcp): reuse cached oauth redirect port on re-auth #49249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -135,6 +135,41 @@ def _find_free_port() -> int: | |
| return s.getsockname()[1] | ||
|
|
||
|
|
||
| def _cached_redirect_port(storage: "HermesTokenStorage | None") -> int | None: | ||
| """Return the loopback callback port from cached client registration. | ||
|
|
||
| OAuth providers bind a dynamically-registered ``client_id`` to the exact | ||
| redirect URI that was registered with it. If Hermes restarts and chooses a | ||
| new random callback port while reusing the stored ``client_id``, providers | ||
| such as Summ reject the authorization request with ``redirect_uri does not | ||
| match any registered URIs``. Reusing the cached redirect port keeps the | ||
| authorization request consistent with the stored client registration. | ||
| """ | ||
| if storage is None: | ||
| return None | ||
|
|
||
| try: | ||
| data = _read_json(storage._client_info_path()) | ||
| except (AttributeError, TypeError, ValueError): | ||
| return None | ||
| if not data: | ||
| return None | ||
|
|
||
| for uri in data.get("redirect_uris") or []: | ||
| try: | ||
| parsed = urlparse(str(uri)) | ||
| except (TypeError, ValueError): | ||
| continue | ||
| if ( | ||
| parsed.scheme == "http" | ||
| and parsed.hostname in {"127.0.0.1", "localhost"} | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This accepts a cached |
||
| and parsed.path == "/callback" | ||
| and parsed.port is not None | ||
| ): | ||
| return int(parsed.port) | ||
| return None | ||
|
|
||
|
|
||
| def _is_interactive() -> bool: | ||
| """Return True if we can reasonably expect to interact with a user.""" | ||
| try: | ||
|
|
@@ -640,13 +675,21 @@ def remove_oauth_tokens(server_name: str) -> None: | |
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def _configure_callback_port(cfg: dict) -> int: | ||
| def _configure_callback_port( | ||
| cfg: dict, | ||
| storage: "HermesTokenStorage | None" = None, | ||
| ) -> int: | ||
| """Pick or validate the OAuth callback port. | ||
|
|
||
| Stores the resolved port into ``cfg['_resolved_port']`` so sibling | ||
| helpers (and the manager) can read it from the same dict. Returns the | ||
| resolved port. | ||
|
|
||
| Port choice precedence: | ||
| 1. explicit ``oauth.redirect_port`` config | ||
| 2. cached client registration redirect URI port | ||
| 3. newly allocated free port | ||
|
|
||
| NOTE: also sets the legacy module-level ``_oauth_port`` so existing | ||
| calls to ``_wait_for_callback`` keep working. The legacy global is | ||
| the root cause of issue #5344 (port collision on concurrent OAuth | ||
|
|
@@ -655,7 +698,7 @@ def _configure_callback_port(cfg: dict) -> int: | |
| """ | ||
| global _oauth_port | ||
| requested = int(cfg.get("redirect_port", 0)) | ||
| port = _find_free_port() if requested == 0 else requested | ||
| port = requested or _cached_redirect_port(storage) or _find_free_port() | ||
| cfg["_resolved_port"] = port | ||
| _oauth_port = port # legacy consumer: _wait_for_callback reads this | ||
| return port | ||
|
|
@@ -762,7 +805,7 @@ def build_oauth_auth( | |
| "initial authorization, then cached tokens will be reused." | ||
| ) | ||
|
|
||
| _configure_callback_port(cfg) | ||
| _configure_callback_port(cfg, storage) | ||
| client_metadata = _build_client_metadata(cfg) | ||
| _maybe_preregister_client(storage, cfg, client_metadata) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please validate the decoded client-info shape and keep URI port extraction inside the malformed-data fallback path. The helper promises
Nonefor unusable registrations, but these accesses can raise instead of reaching_find_free_port().