SEP-2350: accumulate scopes on step-up auth#2676
Conversation
|
Hi @dogacancolak, I read through #2676 carefully while aligning my own server-side step-up PR (TypeScript SDK #1624) with SEP-2350. Posting cross-SDK context in case it helps. On your two questions:
I surveyed other SDKs. It's not clear and I've asked Den.
I think your choice to drop on 401 is well argued. It matches the principle of least privilege, prevents an over-broad accumulated set from outliving a session, and respects the user's opportunity to re-consent, but could lead to lots of re-auth. The "401 is the down-scoping opportunity" sounds correct. That said, SEP-2350 does not actually mandate this. It only mandates client-side union on 403. Two SDKs already go the other way. I am going to raise this with @dend as a spec clarification because silent divergence here will cause real interop pain. I will tag you in that thread.
Your choice to union from The Swift SDK does it from existing token scopes and Rust does it from On the server side (TS SDK #1624): my updated PR now emits only the per-operation Thanks for pulling at these threads. |
Implements MCP spec PR #2350 (closes #2349): on a
403 insufficient_scopestep-up challenge, the client now unions the challenge scopes with its previously-requested scope set instead of replacing them. Stateless servers can now emit per-operationWWW-Authenticatechallenges (the spec-recommended posture) without forcing the client to drop prior grants on every step-up.Motivation and Context
The previous spec told servers to include "previously granted scopes" in
insufficient_scopechallenges, conflicting with RFC 6750 §3.1. The amended spec moves accumulation to the client.The SDK was replacing
client_metadata.scopewith the challenge scopes, so a client doingread→write→adminoperations would lose prior scopes on each step-up and trigger re-auth loops.What changed
src/mcp/client/auth/oauth2.pynow callsunion_scopes(client_metadata.scope, challenge_scopes)instead of overwriting. The 401 branch is unchanged (replace semantics) — full re-login remains the natural down-scoping opportunity.client_metadata.scope(what we requested last time), notcurrent_tokens.scope(what the AS granted). If the AS narrowed the grant at consent time, we still re-request the broader set on the next step-up rather than silently dropping scopes the user previously declined.Behavior
readrequested → 403scope="write"→ re-auth withread write.read writerequested, grantedreadonly → 403scope="delete"→ re-auth withread write delete(write survives the narrow grant).Tests
Unit tests for
union_scopes.End-to-end tests driving
async_auth_flowcover the entire step-up branch, not just the union behavior:read→ step-up addswrite→ step-up addsadminends withread write admin, not justadmin.read write adminbut the AS only grantedread writeat consent, a later step-up fordeletere-authorizes withread write admin delete— the declinedadminis re-requested rather than silently dropped.insufficient-scopedoes not trigger re-auth.read write adminwith the initial auth handshake's narrowerread. Pins the invariant so a future "let's union on 401 too" change fails loudly.Limitations / follow-ups
refresh_tokenflow itself.