@@ -81,22 +81,23 @@ async def test_a_request_with_no_authorization_header_is_challenged_with_resourc
8181 """No `Authorization` header → 401 with a `WWW-Authenticate` carrying `resource_metadata`.
8282
8383 The snapshot pins current behaviour: the SDK collapses the no-header, unknown-token, and
84- expired-token cases into one challenge (`error="invalid_token"`, no `scope` parameter). The
85- spec says the discovery-time challenge SHOULD include `scope` and RFC 6750 says the
86- no-credentials case SHOULD NOT carry an error code; both gaps are recorded as the divergence
87- on this requirement. Asserting the dict equals an exact key set also pins that no parameter
88- appears twice.
84+ expired-token cases into one challenge. The `scope` parameter carries the configured required
85+ scopes ( spec SHOULD, RFC 6750 section 3; #3103). RFC 6750 also says the no-credentials case
86+ SHOULD NOT carry an error code; that remaining gap is recorded as the divergence on this
87+ requirement. Asserting the dict equals an exact key set also pins that no parameter appears
88+ twice.
8989 """
9090 response = await post_mcp (protected )
9191
9292 assert response .status_code == 401
9393 assert response .headers ["www-authenticate" ] == snapshot (
94- 'Bearer error="invalid_token", error_description="Authentication required", '
94+ 'Bearer error="invalid_token", error_description="Authentication required", scope="mcp:read", '
9595 'resource_metadata="http://127.0.0.1:8000/.well-known/oauth-protected-resource/mcp"'
9696 )
9797 assert parse_www_authenticate (response .headers ["www-authenticate" ]) == {
9898 "error" : "invalid_token" ,
9999 "error_description" : "Authentication required" ,
100+ "scope" : REQUIRED_SCOPE ,
100101 "resource_metadata" : RESOURCE_METADATA_URL ,
101102 }
102103 assert response .json () == snapshot ({"error" : "invalid_token" , "error_description" : "Authentication required" })
@@ -106,15 +107,16 @@ async def test_a_request_with_no_authorization_header_is_challenged_with_resourc
106107async def test_an_unrecognized_bearer_token_is_answered_401_invalid_token (protected : httpx2 .AsyncClient ) -> None :
107108 """A token the verifier does not recognize is answered 401 `invalid_token`.
108109
109- The challenge is identical to the no-header case (the backend returns `None` for both); the
110- missing `scope` parameter is the recorded divergence on this requirement .
110+ The challenge is identical to the no-header case (the backend returns `None` for both),
111+ including the `scope` parameter carrying the configured required scopes (#3103) .
111112 """
112113 response = await post_mcp (protected , bearer = "tok-unknown" )
113114
114115 assert response .status_code == 401
115116 assert parse_www_authenticate (response .headers ["www-authenticate" ]) == {
116117 "error" : "invalid_token" ,
117118 "error_description" : "Authentication required" ,
119+ "scope" : REQUIRED_SCOPE ,
118120 "resource_metadata" : RESOURCE_METADATA_URL ,
119121 }
120122
@@ -124,8 +126,7 @@ async def test_an_expired_token_is_answered_401(protected: httpx2.AsyncClient) -
124126 """A token whose `expires_at` is in the past is answered 401 `invalid_token`.
125127
126128 The expiry check is the bearer backend's, against the wall clock; the test seeds a concrete
127- past timestamp so no time mocking is involved. The missing `scope` parameter is the recorded
128- divergence on this requirement.
129+ past timestamp so no time mocking is involved.
129130 """
130131 response = await post_mcp (protected , bearer = "tok-expired" )
131132
@@ -134,26 +135,24 @@ async def test_an_expired_token_is_answered_401(protected: httpx2.AsyncClient) -
134135
135136
136137@requirement ("hosting:auth:scope-403" )
137- async def test_a_token_missing_a_required_scope_is_answered_403_insufficient_scope_without_a_scope_param (
138+ async def test_a_token_missing_a_required_scope_is_answered_403_insufficient_scope_with_a_scope_param (
138139 protected : httpx2 .AsyncClient ,
139140) -> None :
140- """A token lacking the required scope is answered 403 `insufficient_scope`, with no `scope` parameter.
141+ """A token lacking the required scope is answered 403 `insufficient_scope` with a `scope` parameter.
141142
142- The spec's runtime-insufficient-scope guidance says the challenge SHOULD include `scope`
143- naming the required scope; the SDK never emits it, recorded as the divergence on this
144- requirement. The SDK client reads `scope` from this header to drive step-up, so the gap is
145- a resource-server/client asymmetry.
143+ The spec's runtime-insufficient-scope guidance (and RFC 6750 section 3.1) says the challenge
144+ SHOULD include `scope` naming the required scope; the SDK client reads it from this header to
145+ drive step-up authorization (#3103).
146146 """
147147 response = await post_mcp (protected , bearer = "tok-noscope" )
148148
149149 assert response .status_code == 403
150- parsed = parse_www_authenticate (response .headers ["www-authenticate" ])
151- assert parsed == {
150+ assert parse_www_authenticate (response .headers ["www-authenticate" ]) == {
152151 "error" : "insufficient_scope" ,
153152 "error_description" : f"Required scope: { REQUIRED_SCOPE } " ,
153+ "scope" : REQUIRED_SCOPE ,
154154 "resource_metadata" : RESOURCE_METADATA_URL ,
155155 }
156- assert "scope" not in parsed
157156
158157
159158@requirement ("hosting:auth:aud-validation" )
0 commit comments