fix: include RFC 6750 scope parameter in insufficient_scope challenge

Closed
VI
20d ago
+60-2

[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
    exact WWW-Authenticate header value on a 403 with multiple required scopes.
  • Added test_no_user_challenge_omits_scope, asserting the 401 "no user" challenge
    does not carry a scope attribute, to pin the 401/403 distinction.
  • Revert-proofed: reverting the middleware change makes the new positive-case test
    fail with the expected diff (missing scope="..."), 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.