Skip to content

OAuth token refresh hits the wrong endpoint when the auth server lives under a path #3240

Open

Description

@mfadul24

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_endpoint under 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

  1. zsxh1990 commented on Aug 4, 2026

    @zsxh1990

    I'd like to take this on.

    The root cause is clear: async_auth_flow does the eager refresh at the top before any metadata discovery, so _refresh_token falls back to urljoin(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:

    • Refresh uses the real token_endpoint from metadata when available
    • Headless/gateway clients that can't do interactive auth don't silently disconnect on token expiry
    • The fallback path-stripping logic stays as a last resort for servers without metadata

    Relevant 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.

  2. added 3 commits that reference this issue on Aug 4, 2026
    ebff435
    0ee7f76
    a9d322e
  3. Ecocitizenz commented on Aug 4, 2026

    @Ecocitizenz
  4. added
    P1Significant bug affecting many users, highly requested feature
    on Aug 12, 2026
  5. added a commit that references this issue on Aug 18, 2026
    2bc618a
  6. added
    v1Affects the v1.x maintenance line
    v2Affects the v2 line (2.x on main)
    on Aug 18, 2026
  7. zsxh1990 commented on Oct 14, 2026

    @zsxh1990

    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 on client-conformance, server-conformance and zizmor. 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:

    1. The diagnosis in my 2026-08-04 comment still stands — async_auth_flow does the eager refresh before any metadata discovery, so _refresh_token falls back to urljoin(get_authorization_base_url(server_url), "/token"), which strips the path when the authorization server is not at the origin root.

    2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    P1Significant bug affecting many users, highly requested featurebugSomething isn't workingv1Affects the v1.x maintenance linev2Affects the v2 line (2.x on main)

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions