@@ -193,11 +193,8 @@ async def test_a_mismatched_iss_on_the_callback_aborts_the_flow() -> None:
193193 """A callback whose RFC 9207 iss does not match the authorization server issuer aborts the flow.
194194
195195 `iss_override` makes the headless callback return an issuer the AS never advertised; the SDK
196- compares it to `oauth_metadata.issuer` and raises `OAuthFlowError` before the token exchange --
197- the recorded traffic shows no /token POST, so the tainted authorization code is never exchanged.
198- Also pins the mismatch arm of `client-auth:iss:unadvertised-present-validated`: the served AS
199- metadata never advertises `authorization_response_iss_parameter_supported`, so this rejection is
200- RFC 9207 validation table row 3 -- a present iss is validated regardless of advertisement.
196+ compares it to `oauth_metadata.issuer` and raises `OAuthFlowError` before the token exchange.
197+ Also the row-3 mismatch arm: a present iss is validated even though the AS never advertises iss support.
201198 """
202199 recorded , on_request = record_requests ()
203200 provider = InMemoryAuthorizationServerProvider ()
@@ -431,13 +428,8 @@ async def test_an_authorize_error_on_the_callback_aborts_the_flow_before_the_tok
431428async def test_a_token_endpoint_error_response_aborts_the_flow_without_a_bearer_request () -> None :
432429 """A token-endpoint error response aborts the flow as `OAuthTokenError`, and no bearer is ever sent.
433430
434- SDK-defined surfacing (no spec mandate governs client-side token-error handling):
435- `code_override` forges the authorization code at the callback boundary, so the SDK's own
436- token handler produces a genuine RFC 6749 `invalid_grant` 400 -- no shim anywhere. The
437- matched message pins only the SDK-authored prefix naming the HTTP status; the tail is the
438- server handler's JSON, and the absent machine-readable code is the deferred
439- `client-auth:token-error:machine-readable-code`. The recorded traffic proves the failed
440- exchange was not retried and no request ever carried a bearer token.
431+ SDK-defined behaviour. The match pins only the SDK-authored status prefix; the missing
432+ machine-readable error code is the deferred `client-auth:token-error:machine-readable-code`.
441433 """
442434 recorded , on_request = record_requests ()
443435 provider = InMemoryAuthorizationServerProvider ()
@@ -450,21 +442,16 @@ async def test_a_token_endpoint_error_response_aborts_the_flow_without_a_bearer_
450442 ):
451443 await connect_with_oauth (server , provider = provider , headless = headless , on_request = on_request ).__aenter__ ()
452444
453- # The flow genuinely reached the token step via a real authorize round trip.. .
445+ # Guards that the failure happened at the token step, not earlier in the flow .
454446 assert find (recorded , "GET" , "/authorize" ) != []
455- # ...the failed exchange was not retried (no loop)...
456447 assert len (find (recorded , "POST" , "/token" )) == 1
457- # ...and no request ever carried a bearer: the failure pre-empted token use entirely.
458448 assert all ("authorization" not in r .headers for r in find (recorded , "POST" , "/mcp" ))
459449
460450
461451def canned_asm (* , iss_advertised : bool | None ) -> dict [str , bytes ]:
462- """Build a `serve=` override: canned AS metadata with a slash-explicit issuer literal .
452+ """Build a `serve=` override: canned AS metadata pinning the iss-advertisement arm .
463453
464- The SDK server's `build_metadata` never sets `authorization_response_iss_parameter_supported`,
465- so the advertising arms of the RFC 9207 validation table need this override; `None` omits the
466- field (`exclude_none`), keeping the document an unadvertising AS whose issuer bytes are still
467- harness-pinned.
454+ Needed because the SDK server's `build_metadata` never advertises iss support; `None` omits the field.
468455 """
469456 override = OAuthMetadata (
470457 issuer = AnyHttpUrl (f"{ BASE_URL } /" ),
@@ -483,13 +470,8 @@ def canned_asm(*, iss_advertised: bool | None) -> dict[str, bytes]:
483470async def test_a_matching_iss_lets_the_flow_redeem_the_code_when_the_as_advertises_iss_support () -> None :
484471 """A callback iss equal to the recorded metadata issuer proceeds to redeem the code (RFC 9207 table row 1).
485472
486- Spec-mandated: advertised support + present matching iss -> compare and proceed. The shim
487- serves AS metadata advertising `authorization_response_iss_parameter_supported`; the suite's
488- provider stamps the matching iss on the success redirect. `headless.iss` proves the callback
489- really carried the recorded issuer (completion alone could also mean the value was never
490- looked at -- the rejection arms of the family are the discriminators). Whether the SDK
491- consulted the advertisement flag is not asserted: rows 1 and 3 share one unconditional
492- comparison branch, so the flag's effect is only observable on the absent-iss arms.
473+ Spec-mandated. `headless.iss` proves the callback really carried the issuer; whether the SDK
474+ consulted the advertisement flag is only observable on the absent-iss arms, so it is not asserted.
493475 """
494476 recorded , on_request = record_requests ()
495477 provider = InMemoryAuthorizationServerProvider ()
@@ -509,7 +491,6 @@ async def test_a_matching_iss_lets_the_flow_redeem_the_code_when_the_as_advertis
509491 assert result .tools [0 ].name == "echo"
510492 assert storage .tokens is not None
511493 assert headless .iss == f"{ BASE_URL } /"
512- # One authorize and one token exchange: the flow proceeded directly, no rejection/retry loop.
513494 assert len (find (recorded , "GET" , "/authorize" )) == 1
514495 assert len (find (recorded , "POST" , "/token" )) == 1
515496
@@ -518,14 +499,8 @@ async def test_a_matching_iss_lets_the_flow_redeem_the_code_when_the_as_advertis
518499async def test_an_iss_differing_only_by_a_trailing_slash_is_rejected_without_normalization () -> None :
519500 """An iss equal to the recorded issuer up to a trailing slash is a mismatch: nothing is normalized away.
520501
521- Spec-mandated: the comparison is RFC 9207 simple string comparison, and the client MUST NOT
522- apply scheme or host case folding, default-port elision, trailing-slash, or percent-encoding
523- normalization before comparing; the trailing-slash arm is pinned as the representative class
524- (the SDK's comparison is a single string inequality). Both spellings are harness literals:
525- the canned metadata carries the slash-explicit issuer and the provider stamps the slash-less
526- redirect iss. Natural metadata would instead source the slash from the SDK server's issuer
527- serialization -- the load-bearing, churn-prone arm of that difference -- whereas the client's
528- metadata parse preserves the served bytes and contributes nothing to it.
502+ Spec-mandated: RFC 9207 simple string comparison with no normalization; the trailing-slash
503+ arm is pinned as the representative class (the SDK's comparison is a single string inequality).
529504 """
530505 recorded , on_request = record_requests ()
531506 provider = InMemoryAuthorizationServerProvider (issuer = BASE_URL )
@@ -550,10 +525,7 @@ async def test_an_iss_differing_only_by_a_trailing_slash_is_rejected_without_nor
550525async def test_a_missing_iss_is_rejected_when_the_as_advertises_iss_support () -> None :
551526 """A callback without iss is rejected before the code is redeemed when the AS advertises iss support (row 2).
552527
553- Spec-mandated: advertised support + absent iss -> reject. The SDK's whole authorization-response
554- input is the callback's `AuthorizationCodeResult`, so the absence is constructed at that boundary
555- with `omit_iss` (the suite's AS emits iss on every success redirect; the redirect itself is
556- unchanged). No /token POST is recorded -- the authorization code is never exchanged.
528+ Spec-mandated. The callback is the SDK's whole authorization-response input, so `omit_iss` removes iss there.
557529 """
558530 recorded , on_request = record_requests ()
559531 provider = InMemoryAuthorizationServerProvider ()
@@ -584,11 +556,8 @@ async def test_a_missing_iss_is_rejected_when_the_as_advertises_iss_support() ->
584556async def test_a_missing_iss_is_tolerated_when_the_as_does_not_advertise_iss_support () -> None :
585557 """A callback without iss proceeds with the code exchange when the AS does not advertise iss support (row 4).
586558
587- Spec-mandated: no advertisement + absent iss -> proceed. The SDK server's `build_metadata`
588- never sets `authorization_response_iss_parameter_supported`, so the natural metadata route IS
589- the unadvertising AS; if it ever started advertising, this test would fail loudly (absent iss
590- + advertised -> the row-2 reject), so the precondition cannot silently rot. The absent iss is
591- constructed at the callback boundary with `omit_iss`.
559+ Spec-mandated. Natural metadata is already the unadvertising arm; if the SDK server ever
560+ started advertising, absent iss would hit the row-2 reject, so the precondition cannot rot.
592561 """
593562 recorded , on_request = record_requests ()
594563 provider = InMemoryAuthorizationServerProvider ()
@@ -604,20 +573,15 @@ async def test_a_missing_iss_is_tolerated_when_the_as_does_not_advertise_iss_sup
604573
605574 assert result .tools [0 ].name == "echo"
606575 assert storage .tokens is not None
607- # Exactly one token exchange: the flow proceeded directly, no rejection/retry detour.
608576 assert len (find (recorded , "POST" , "/token" )) == 1
609577
610578
611579@requirement ("client-auth:iss:unadvertised-present-validated" )
612580async def test_a_present_iss_is_validated_and_accepted_even_when_the_as_does_not_advertise_support () -> None :
613581 """A present iss is compared against the recorded issuer even without metadata advertisement (row 3, match arm).
614582
615- Spec-mandated, and the point where the MCP spec deliberately exceeds RFC 9207's local-policy
616- provision: a present iss is validated regardless of advertisement. The natural AS metadata is
617- the unadvertising AS and the provider stamps the matching iss; `headless.iss` proves it was
618- present on the callback. The match arm alone cannot prove the comparison ran -- the same
619- entry's mismatch arm is pinned by `test_a_mismatched_iss_on_the_callback_aborts_the_flow`,
620- which drives a mismatched iss against the same unadvertising AS.
583+ Spec-mandated; MCP exceeds RFC 9207's local-policy provision here. The mismatch arm is pinned
584+ by `test_a_mismatched_iss_on_the_callback_aborts_the_flow`.
621585 """
622586 recorded , on_request = record_requests ()
623587 provider = InMemoryAuthorizationServerProvider ()
@@ -641,14 +605,8 @@ async def test_a_present_iss_is_validated_and_accepted_even_when_the_as_does_not
641605async def test_an_error_redirect_with_a_mismatched_iss_is_rejected_on_iss_before_the_missing_code_error () -> None :
642606 """iss validation applies equally to error responses: the mismatch is raised before the missing-code error.
643607
644- Spec-mandated at 2026-07-28 (SEP-2468): the AS denies the authorize request, producing a real
645- RFC 6749 error redirect (`error=access_denied`, no code, no iss), and `iss_override` supplies
646- the mismatching issuer at the callback boundary -- the SDK never parses a callback URL, so its
647- whole authorization-response input is the callback's `AuthorizationCodeResult`. Raising the
648- iss mismatch in preference to "No authorization code received" (the arm the error-surfaces
649- test above pins) is the observable proving the validation ran on an error response. The
650- MUST-NOT-act-on-or-display half is not asserted: the callback contract carries no error
651- fields, so the negative would be vacuously true by construction (the manifest note records it).
608+ Spec-mandated at 2026-07-28 (SEP-2468). The mismatch pre-empting the missing-code error proves
609+ validation ran; the MUST-NOT-act-on half is vacuous (no error fields in the callback contract).
652610 """
653611 recorded , on_request = record_requests ()
654612 provider = InMemoryAuthorizationServerProvider (deny_authorize = True )
@@ -661,7 +619,6 @@ async def test_an_error_redirect_with_a_mismatched_iss_is_rejected_on_iss_before
661619 ):
662620 await connect_with_oauth (server , provider = provider , headless = headless , on_request = on_request ).__aenter__ ()
663621
664- # The callback genuinely was an error response, and the tainted exchange never started.
665622 assert headless .error == "access_denied"
666623 # The recorded unauthenticated trigger POST guards the negative below against an unwired hook.
667624 assert find (recorded , "POST" , "/mcp" ) != []
0 commit comments