feat: accumulate client-side scopes during step-up authorization (SEP-2350)#888
Conversation
SEP-2350 [1] clarifies that scope accumulation is a client-side responsibility: during re-authorization the client requests the union of its previously requested scopes and the newly challenged scopes, because servers report only the scopes needed for the current operation in 403 / insufficient_scope challenges (RFC 6750 §3.1), not the union of everything granted so far. select_base_scopes returned a single source, so a 403 challenge replaced the previously requested scopes instead of widening them, dropping prior permissions across step-up rounds. I make it union the previously requested scopes, the WWW-Authenticate challenge, and the protected resource metadata scopes (RFC 9728), treating each server-reported set as an operational requirement for the current operation rather than an exclusive directive; AS metadata and caller defaults only seed the request when nothing has been requested or challenged yet. exchange_code_for_token treats an explicit scope list as authoritative, so a server may still narrow the grant, but retains the accumulated set when the server omits scope (RFC 6749 §5.1) rather than clearing it. Deduplication preserves first-seen order for stable, testable output. Tests cover multi-round accumulation, dedup, and resource-metadata unioning. Implements [2]. [1]: https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/docs/specification/draft/basic/authorization.mdx#L682 [2]: modelcontextprotocol#877 Signed-off-by: Stefano Amorelli <stefano@amorelli.tech>
94c4a47 to
563280f
Compare
|
nice @stefanoamorelli - only thought so far Looks like that should be fixed: https://www.rfc-editor.org/info/rfc6749/#section-5.1 so I think that is all, if that can be patched then this could be good to go. |
michaelneale
left a comment
There was a problem hiding this comment.
Around the exchange_code_for_token change, roughly original/current PR hunk near line 1239:
let granted_scopes: Vec<String> = match token_result.scopes() {
Some(scopes) => scopes.iter().map(|s| s.to_string()).collect(),
None => self.current_scopes.read().await.clone(),
};as mentioned in other comment, I think that should be the scopes requested, not the current scopes?
Closes #877
Motivation and Context
SEP-2350 makes scope accumulation a client responsibility. A
403/insufficient_scopechallenge carries only the scopes for the current operation (RFC 6750 §3.1), so on re-authorization the client must request the union of what it asked for before and what was just challenged.select_base_scopes()returned a single source, so a challenge replaced the earlier scopes instead of widening them, and step-up dropped previously granted permissions (theauth/scope-step-upconformance test failed for this). This PR unionscurrent_scopes, the WWW-Authenticate challenge, and the resource metadata scopes (RFC 9728); defaults only seed the first request. On exchange, an explicitscopelist stays authoritative so a server can still narrow the grant, but when the server omitsscope(RFC 6749 §5.1) the accumulated set is retained rather than cleared.How Has This Been Tested?
Added unit tests for multi-round accumulation, union-not-replace, dedup of an already-requested challenge, and resource metadata accumulation. The rmcp suite passes with the
authfeature, andcargo +nightly fmt --alland clippy are clean.Breaking Changes
No API changes. The behavior change is required by the SEP: re-authorization requests the union of prior and challenged scopes.
exchange_code_for_tokenstill honors an explicit downgrade from the server and only retains the accumulated set when the server omitsscope, so a previously granted scope is not reported back unless the server kept granting it.Types of changes
Checklist
Additional context
This is the client half of SEP-2350; server-side per-operation scope reporting is out of scope for the SDK. No new error handling was needed since the change only widens the requested set.