Built-in server-side incremental scope consent (SEP-835)#1483
Built-in server-side incremental scope consent (SEP-835)#1483
Conversation
Agent-Logs-Url: https://github.com/modelcontextprotocol/csharp-sdk/sessions/5671ae62-779a-42f5-b84c-77d470131732 Co-authored-by: mikekistler <85643503+mikekistler@users.noreply.github.com>
| var scope = primitive.Metadata | ||
| .OfType<IAuthorizeData>() | ||
| .Select(static a => a.Roles) | ||
| .FirstOrDefault(static r => !string.IsNullOrEmpty(r)); |
| CheckReadResourceFilter(options); | ||
|
|
||
| CheckListPromptsFilter(options); | ||
| CheckGetPromptFilter(options); |
There was a problem hiding this comment.
I do see the appeal of doing authz before reaching the filter stack, considering that we flush the headers before any filters/handlers run making impossible to change the status code at that point.
However, I don't know removing these checks is the right move. If we were going to do something like that, we would probably just want to remove the authz filters altogether and move it entirely to the Streamable HTTP handler. Of course this would make it impossible to run filters before authz.
Right now, it looks like we're forced to deserialize the payload twice for Stremable HTTP. Of course, you also cannot really remove the authz filters without moving the auth checks into the SseHandler too.
I think incremental step up is niche enough it should be opt-in. And I think you should be required to add the authz filters regardless if you have any handlers attributed with [Authorize], even if we do have a way to short-circuit for incremental consent in the StreamableHttpHandler for some possible subset of. If we were sure that all the attributed handlers all relied exclusively on incremental consent, that might be different.
|
|
||
| // Signal to the HTTP transport that authorization filters are handling access control, | ||
| // so the pre-flight incremental scope consent check (SEP-835) should be skipped. | ||
| builder.Services.Configure<HttpServerTransportOptions>(static o => o.AuthorizationFiltersRegistered = true); |
There was a problem hiding this comment.
Wouldn't it make sense to allow a subset of handlers require incremental concent instead of being all or nothing?
| if (policyProvider is null) | ||
| { | ||
| // No authorization infrastructure configured; skip the pre-flight check. | ||
| return false; |
There was a problem hiding this comment.
If we're not requiring the filters to be registered, this probably needs to throw like the filters do.
csharp-sdk/src/ModelContextProtocol.AspNetCore/AuthorizationFilterSetup.cs
Lines 330 to 332 in 1492a5d
| /// authorization check that returns HTTP 403 with <c>WWW-Authenticate: Bearer error="insufficient_scope"</c> | ||
| /// to trigger client re-authentication with broader scopes. | ||
| /// </summary> | ||
| public class IncrementalConsentTests(ITestOutputHelper testOutputHelper) : KestrelInMemoryTest(testOutputHelper) |
There was a problem hiding this comment.
I figured this would derive from OAuthTestBase, so we could test the entire process. Even if we had that though, I would be hesitant to ship advanced server-side OAuth features without verifying compatiblity with a decent number of OAuth providers.
In my experience, they tend to be pretty incompatible. I know the MCP spec requires support for this flow from the client at least, but Entra doesn't really support MCP-style resource-parameter-based OAuth yet. I'd want to verify that works before doing more.
|
I'm unhappy with this first pass from copilot and working on revising it with the local agent. Hoping to push up a revision shortly. |
|
Do you have any real OAuth servers you're manually testing with? I know it'd be hard to automate anything relying on a cloud-based OAuth server. I've been there with Entra, but I definitely want to know which popular servers this works with out of the box. We can always wait for wider support if it isn't there yet just to make sure what we do is widely compatible. |
Adds server-side support for incremental scope consent so MCP servers can decorate primitives with
[Authorize]without callingAddAuthorizationFilters(), and the SDK automatically returns HTTP 403 with properWWW-Authenticateheaders to trigger client re-authentication with broader scopes. The existingClientOAuthProvideralready handles these 403 responses client-side.Behavior
AddAuthorizationFilters()calledWWW-AuthenticateUsage
Unauthorized call returns:
Changes
AuthorizationFilterSetup— RemovedPostConfigurecheck filters that threwInvalidOperationExceptionwhen[Authorize]was found withoutAddAuthorizationFilters(); those checks blocked the new incremental consent path. MadeHasAuthorizationMetadataandCombineAsyncinternalfor reuse.AddAuthorizationFilters()behavior is unchanged.HttpServerTransportOptions— Added internalAuthorizationFiltersRegisteredflag.HttpMcpServerBuilderExtensions.AddAuthorizationFilters()— SetsAuthorizationFiltersRegistered = trueso the new pre-flight check skips when the existing filter pipeline is in use.StreamableHttpHandler— AddedTryHandleInsufficientScopeAsync()called beforeInitializeSseResponse(). Fortools/call,prompts/get, andresources/readtargeting a primitive with auth metadata, evaluates the policy and writes HTTP 403 +WWW-Authenticate: ******"insufficient_scope", scope="<roles>", resource_metadata="..."if unauthorized. The check must run before SSE headers are committed (HTTP 200).AuthorizeAttributeTests— Updated 7 existingWithoutAuthFilterstests to reflect the new behavior (lists succeed and return all primitives; invocations return HTTP 403).IncrementalConsentTests(new) — Tests for: all primitives visible in listings, HTTP 403 + correctWWW-Authenticateheader for unauthorized invocations, successful invocation for authorized users, and thatAddAuthorizationFilters()behavior is unchanged.