OAuth token refresh hits the wrong endpoint when the auth server lives under a path #3240
Description
Ran into this with a hosted MCP server whose authorization server isn't at the origin root — token endpoint is https://host/oauth2/api/v1/token, not https://host/token.
If a client starts up with a cached-but-expired access token (+ refresh token) and hasn't done discovery yet, async_auth_flow refreshes at the very top — before any 401/metadata discovery. So oauth_metadata is None and _refresh_token uses the fallback urljoin(get_authorization_base_url(server_url), "/token"), i.e. just {scheme}://{netloc}/token. That 404s, _handle_refresh_response clears the tokens, and the flow drops to full interactive auth — which a headless/gateway client can't do. So the server silently disconnects every time the access token expires (mine are 5 min, so… constantly).
Same path-stripping fallback is in _get_token_endpoint, _perform_authorization_code_grant (/authorize) and DCR (/register) — refresh is just the one that bites silently.
Repro (roughly):
- MCP server whose AS metadata puts
token_endpointunder a path, not{origin}/token - log in normally so tokens get cached
- let the access token expire (or clear the expiry), reconnect with a fresh provider
- watch the refresh POST go to
https://host/token→ 404 → "Token refresh failed" → tokens cleared → it tries to open a browser
Fix looks like: discover metadata before the eager refresh (or stop dropping the issuer path in the fallback). Happy to PR — have a branch that pulls the PRM/ASM discovery out of the 401 branch and runs it before the refresh.
(used some AI help digging into this)
Activity
zsxh1990 commentedon Aug 4, 2026
I'd like to take this on.
The root cause is clear:
async_auth_flowdoes the eager refresh at the top before any metadata discovery, so_refresh_tokenfalls back tourljoin(get_authorization_base_url(server_url), "/token")— which strips the path when the AS isn't at the origin root.Proposed fix: Pull PRM/ASM discovery (
_get_oauth_metadata/_get_authorization_server_metadata) out of the 401-handling branch and run it eagerly before the refresh attempt. If discovery fails (no AS metadata endpoint), fall back to the current behavior. This way:token_endpointfrom metadata when availableRelevant experience: Contributed #3093 (non-2xx response tests for the Python SDK), familiar with the auth flow internals.
Happy to open a PR if this direction looks right.
fix: skip eager token refresh when OAuth metadata is unavailable
test: verify eager refresh is skipped when oauth_metadata is None
fix: skip eager token refresh when OAuth metadata is unavailable
Ecocitizenz commentedon Aug 4, 2026
OAuth client: refresh before re-authorizing, and discover before refr…
zsxh1990 commentedon Oct 14, 2026
Withdrawing my claim on this from 2026-08-04 ("I'd like to take this on").
I never followed up, and there is already a working fix in flight: PR #3263 (
fix(client/auth): discover AS metadata before cold-start token refresh, with issuer-binding check) has been open since 2026-08-07, has been through nine review rounds with all findings confirmed fixed, and is green onclient-conformance,server-conformanceandzizmor. Leaving a stale claim here makes the issue look taken when the only thing actually moving is that PR.Two notes for whoever picks this up, neither of which is a request for assignment:
The diagnosis in my 2026-08-04 comment still stands —
async_auth_flowdoes the eager refresh before any metadata discovery, so_refresh_tokenfalls back tourljoin(get_authorization_base_url(server_url), "/token"), which strips the path when the authorization server is not at the origin root.The open review note on fix(client/auth): discover AS metadata before cold-start token refresh, with issuer-binding check #3263 is worth weighing before merge: the fix adds roughly 120 lines, and the reviewer asked for a judgement call on whether that is proportionate for a fallback-path bug. That is a design question, not a correctness one.
Not asking to be assigned — just making the record honest. The work is not claimed by me any more.