Skip to content

WWW-Authenticate parsing matches a field name as a substring of another auth-param #3009

Open

Description

@VihaanAgarwal

Describe the bug

Ran into this wiring up an OAuth-protected MCP client. When a 401 WWW-Authenticate challenge carries several auth-params, extract_field_from_www_auth (mcp/client/auth/utils.py) matches the requested field name as a substring of another param's name, so a different param shadows the real value.

The pattern is rf'{field_name}=(?:"([^"]+)"|([^\s,]+))' searched with re.search, with no boundary before field_name.

To reproduce

import httpx
from mcp.client.auth.utils import extract_field_from_www_auth, extract_resource_metadata_from_www_auth

def r(h):
    return httpx.Response(401, headers={"WWW-Authenticate": h},
                          request=httpx.Request("GET", "https://api.example.com/"))

# A real `scope` is present, but `error_scope` shadows it:
print(extract_field_from_www_auth(r('Bearer error_scope="decoy", scope="read write"'), "scope"))
# -> 'decoy'   (expected 'read write')

# Only a decoy param exists; no real `scope`:
print(extract_field_from_www_auth(r('Bearer custom_scope="leaked"'), "scope"))
# -> 'leaked'  (expected None)

# Same for resource_metadata, which drives discovery URL selection:
print(extract_resource_metadata_from_www_auth(r('Bearer x_resource_metadata="https://decoy.example.com"')))
# -> 'https://decoy.example.com'  (expected None)

Expected behavior

The field name should match only as a complete auth-param name (at the header start or after a whitespace/comma separator), so scope doesn't match inside error_scope. This matters most for resource_metadata, since the client uses it to pick the protected-resource-metadata discovery URL — reading it from the wrong param points discovery at the wrong place.

Environment

  • mcp main (reproduces on v1.x as well)

I have a small fix (anchor the param name to the header start or a separator) plus regression cases added to the existing test_extract_field_from_www_auth_* parametrized tests, and can open a PR if this looks right.

Reviewed by AI

Activity

  1. added
    v2Affects the v2 line (2.x on main)
    on Jun 30, 2026
  2. fede-kamel commented on Jul 10, 2026

    @fede-kamel

    Three open PRs fix this (#3012, #3041, #3050). I ran all three against the same differential probe battery (plus current main as baseline) to help triage — full harness output below. All three fix the reported shadowing, and all three pass tests/client/test_auth.py with zero failures on their branches (137/139/142 passed for #3050/#3041/#3012 — each adds a different number of regression tests). They differ on edge cases:

    case (header → requested field) main #3041 #3050 #3012
    Bearer error_scope="admin", scope="files" → scope ❌ admin ✅ ✅ ✅
    Bearer x_resource_metadata=…, resource_metadata=… → resource_metadata ❌ shadowed ✅ ✅ ✅
    Bearer as_scope=x, scope=files (unquoted) → scope ❌ x ✅ ✅ ✅
    Bearer error_scope="a",scope="files" (no space after comma) → scope ❌ a ✅ ✅ ✅
    Basic realm="r", Bearer scope="files" (two challenges) → scope ✅ ✅ ✅ ❌ None
    DPoP algs="ES256", Bearer scope="files" (RFC 9449 co-advertisement) → scope ✅ ✅ ✅ ❌ None
    Newauth realm="apps" → realm (non-Bearer scheme, generic use) ✅ ❌ None ✅ ✅
    Bearer scope="say \"hi, there" (escaped quote + comma in value) → scope ❌ truncated ❌ truncated ❌ truncated ✅
    Bearer SCOPE="files" → scope (RFC 7235 auth-param names are case-insensitive) ❌ ❌ ❌ ❌

    Summary of the trade-offs:

    • fix: match complete www-authenticate auth params #3050 is the minimal change (anchor the existing regex at a ^|[\s,] boundary + re.escape the field name). No behavior change in any probe other than the fixes. It does not handle escaped quotes, but neither does main, so that is a pre-existing limitation rather than a regression.
    • fix(auth): match complete WWW-Authenticate parameters #3012 is the only one that handles RFC 9110 escaped quotes, but it regresses multi-challenge headers: it treats everything after the first space as auth-params of one challenge, so a Bearer challenge that is not first (Basic …, Bearer … / DPoP …, Bearer …) stops being found. That pattern is real — RFC 9449 servers co-advertise DPoP and Bearer.
    • Fix substring matches in WWW-Authenticate parsing #3041 scopes extraction to the Bearer challenge, which is arguably the right semantics for the RFC 6750 scope / RFC 9728 resource_metadata callers — but it changes the generic contract of extract_field_from_www_auth (a realm lookup on a non-Bearer challenge now returns None where main returned the value). If that is the intended direction it should be explicit in the docstring/signature rather than implicit.

    The param-name case-insensitivity row fails on all four — trivial to add via re.IGNORECASE on the #3050 approach if wanted.

  3. fede-kamel commented on Jul 10, 2026

    @fede-kamel

    @maxisbey since three competing fixes are open for this, the differential matrix above might save a triage pass — TL;DR: #3050 is the minimal no-regression fix; #3012 adds proper escaped-quote parsing but currently regresses multi-challenge headers (live-server repro on the PR); #3041 works but narrows the helper to Bearer-only semantics. Happy to re-run the harness against any revision.

  4. added
    bugSomething isn't working
    P2Moderate issues affecting some users, edge cases, potentially valuable feature
    authIssues and PRs related to Authentication / OAuth
    on Aug 14, 2026
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

    P2Moderate issues affecting some users, edge cases, potentially valuable featureauthIssues and PRs related to Authentication / OAuthbugSomething isn't workingv2Affects 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