fix: include RFC 6750 scope parameter in insufficient_scope challenge
[v1.x] fix: include RFC 6750 scope parameter in insufficient_scope challenge
Fixes #3103
Problem
RequireAuthMiddleware._send_auth_error builds the WWW-Authenticate challenge for
both the 401 (invalid_token) and 403 (insufficient_scope) responses, but never
includes the scope attribute — even when required_scopes is already known on the
middleware instance.
Per RFC 6750 §3.1, the
insufficient_scope response:
The request requires higher privileges than provided by the access token. The
resource server SHOULD respond with the HTTP 403 (Forbidden) status code and MAY
include the "scope" attribute with the scope necessary to access the protected
resource.
Without scope in the challenge, a client can't discover the required scope directly
from the 403 response. The SDK's own client already implements the consumer side of
this — extract_scope_from_www_auth() / get_client_metadata_scopes() treat the
challenge's scope as the highest-priority source when handling step-up
authorization — but the server never emits it, so that path is always empty and the
client falls back to the lower-priority protected-resource-metadata
scopes_supported.
Fix
_send_auth_error now accepts an optional scope argument and appends
scope="..." to the challenge when set. It's only passed from the
insufficient_scope (403) call site, using the full required_scopes list.
The 401 paths (missing/invalid token) are deliberately left unchanged. RFC 6750 §3.1
draws a line between "no authentication info at all" (server SHOULD NOT include error
info) and invalid_token versus insufficient_scope — scope is specific to the
latter, since on a bare 401 the client hasn't necessarily attempted the resource with
any token yet, and there's no established "the request lacked X" scope gap to report
back.
Example, for a server configured with required_scopes=["api.read"]:
Before: WWW-Authenticate: Bearer error="insufficient_scope", error_description="Required scope: api.read"
After: WWW-Authenticate: Bearer error="insufficient_scope", error_description="Required scope: api.read", scope="api.read"
Testing
- Added
test_missing_required_scope_includes_scope_in_challenge, asserting the
exactWWW-Authenticateheader value on a 403 with multiple required scopes. - Added
test_no_user_challenge_omits_scope, asserting the 401 "no user" challenge
does not carry ascopeattribute, to pin the 401/403 distinction. - Revert-proofed: reverting the middleware change makes the new positive-case test
fail with the expected diff (missingscope="..."), while the negative-case test
is unaffected. uv run --frozen pytest(full suite): 1150 passed, 95 skipped, 1 xfailed.uv run --frozen ruff check ./ruff format --check .: clean.uv run --frozen pyright: 0 errors.uv run --frozen coverage run -m pytest ... && coverage report: 100% (repo requires
fail_under = 100).
This targets v1.x (non-breaking bug fix) rather than main, per this repo's
branching table in AGENTS.md. The same gap exists on main/v2 in the equivalent
code path; happy to open a follow-up there if a maintainer wants it, but wanted to
keep this PR scoped to the fix under discussion in the issue.
Credit to @velias for the clear diagnosis and root-cause writeup in the issue.
AI assistance disclosure: drafted with AI assistance (Claude), reviewed and owned
by me before submission, per this repo's AI-Assisted Contributions policy.