@@ -3625,3 +3625,91 @@ async def test_eager_refresh_skips_discovery_when_metadata_already_known(
36253625 assert refresh_request .method == "POST"
36263626 assert str (refresh_request .url ) == "https://auth.example.com/oauth2/api/v1/token"
36273627 await auth_flow .aclose ()
3628+
3629+
3630+ @pytest .mark .anyio
3631+ async def test_eager_refresh_treats_issuer_mismatched_asm_as_failed_discovery (
3632+ oauth_provider : OAuthClientProvider , valid_tokens : OAuthToken
3633+ ):
3634+ """An eagerly probed ASM whose issuer fails SEP-2468 validation is skipped, not fatal.
3635+
3636+ On the hint-less path a mismatched issuer cannot brick the flow: the document is
3637+ ignored, the remaining fallback URLs are tried, and the refresh falls through to
3638+ ``{origin}/token`` — the anchored 401 path still applies the authoritative check.
3639+ """
3640+ oauth_provider .context .current_tokens = valid_tokens
3641+ oauth_provider .context .token_expiry_time = time .time () - 100 # expired
3642+ oauth_provider .context .client_info = OAuthClientInformationFull (
3643+ client_id = "test_client" ,
3644+ redirect_uris = [AnyUrl ("http://localhost:3030/callback" )],
3645+ token_endpoint_auth_method = "none" ,
3646+ )
3647+ oauth_provider ._initialized = True
3648+
3649+ auth_flow = oauth_provider .async_auth_flow (httpx2 .Request ("GET" , "https://api.example.com/v1/mcp" ))
3650+
3651+ # PRM discovery succeeds and points at auth.example.com.
3652+ prm_request = await auth_flow .__anext__ ()
3653+ prm_response = httpx2 .Response (
3654+ 200 ,
3655+ content = (
3656+ b'{"resource": "https://api.example.com/v1/mcp", "authorization_servers": ["https://auth.example.com"]}'
3657+ ),
3658+ request = prm_request ,
3659+ )
3660+
3661+ # First ASM URL answers with a mismatched issuer (SEP-2468): skipped, next URL tried.
3662+ asm_request = await auth_flow .asend (prm_response )
3663+ assert str (asm_request .url ) == "https://auth.example.com/.well-known/oauth-authorization-server"
3664+ mismatched_asm = httpx2 .Response (
3665+ 200 ,
3666+ content = (
3667+ b'{"issuer": "https://internal.example.com", '
3668+ b'"authorization_endpoint": "https://internal.example.com/authorize", '
3669+ b'"token_endpoint": "https://internal.example.com/token"}'
3670+ ),
3671+ request = asm_request ,
3672+ )
3673+ asm_request = await auth_flow .asend (mismatched_asm )
3674+ assert str (asm_request .url ) == "https://auth.example.com/.well-known/openid-configuration"
3675+
3676+ # The fallback URL 404s; the refresh falls through to {origin}/token, no raise.
3677+ refresh_request = await auth_flow .asend (httpx2 .Response (404 , request = asm_request ))
3678+ assert refresh_request .method == "POST"
3679+ assert str (refresh_request .url ) == "https://api.example.com/token"
3680+ assert oauth_provider .context .oauth_metadata is None
3681+ await auth_flow .aclose ()
3682+
3683+
3684+ @pytest .mark .anyio
3685+ async def test_eager_discovery_interrupted_mid_probe_is_retried_on_the_next_refresh (
3686+ oauth_provider : OAuthClientProvider , valid_tokens : OAuthToken
3687+ ):
3688+ """An aborted probe sequence is not recorded as a completed discovery attempt.
3689+
3690+ httpx acloses the auth flow when a probe fails at the transport level; the
3691+ completion flag must stay unset so the next refresh retries discovery instead of
3692+ permanently falling back to ``{origin}/token`` against a server whose token
3693+ endpoint lives elsewhere.
3694+ """
3695+ oauth_provider .context .current_tokens = valid_tokens
3696+ oauth_provider .context .token_expiry_time = time .time () - 100 # expired
3697+ oauth_provider .context .client_info = OAuthClientInformationFull (
3698+ client_id = "test_client" ,
3699+ redirect_uris = [AnyUrl ("http://localhost:3030/callback" )],
3700+ token_endpoint_auth_method = "none" ,
3701+ )
3702+ oauth_provider ._initialized = True
3703+
3704+ # First attempt: the transport dies during the first probe; httpx acloses the flow.
3705+ auth_flow = oauth_provider .async_auth_flow (httpx2 .Request ("GET" , "https://api.example.com/v1/mcp" ))
3706+ first_probe = await auth_flow .__anext__ ()
3707+ assert "oauth-protected-resource" in str (first_probe .url )
3708+ await auth_flow .aclose ()
3709+ assert not oauth_provider .context .eager_discovery_attempted
3710+
3711+ # Next refresh retries discovery from the start rather than skipping to the fallback.
3712+ auth_flow = oauth_provider .async_auth_flow (httpx2 .Request ("GET" , "https://api.example.com/v1/mcp" ))
3713+ retried_probe = await auth_flow .__anext__ ()
3714+ assert str (retried_probe .url ) == "https://api.example.com/.well-known/oauth-protected-resource/v1/mcp"
3715+ await auth_flow .aclose ()
0 commit comments