feat: Add Enterprise Managed Authorization (SEP-990) support#1305
Conversation
|
Hey @halter73, just a friendly ping on this one! It's been a few weeks since our last exchange and I wanted to check if you had any further thoughts on the token refresh / For context, both the Go (go-sdk#770) and TS (typescript-sdk#1531) PRs are still in review too, so I'm happy to align on whatever approach the team lands on across SDKs. I can also keep this PR scoped to the current API surface and follow up with a separate PR for the Happy to make any adjustments - just let me know! |
|
@halter73 Quick update: The TypeScript SDK's SEP-990 PR (typescript-sdk#1531) just merged! 🎉 This confirms the pattern we've implemented here is solid - token refresh via full flow re-execution, no scope step-up support, and caching with expiry checks. All aligned with how the TS SDK handles it. |
c945019 to
ed87d4b
Compare
|
Would it be possible to add OAuthTestBase integration test for this? You might need to add features to the TestOAuthServetlr for it, but I don't think it should be too hard. It's not like it needs to be hardened or anything |
|
Hi @halter73, done! Added 3 One question: do we need a conformance test for this as well, or are the integration tests sufficient? |
halter73
left a comment
There was a problem hiding this comment.
I wonder if we need top-level documentation for this in https://csharp.sdk.modelcontextprotocol.io/concepts/index.html. Or do we assume that each authorization provider (Entra, Okta, etc.) will provide their own docs for each SDK that's specific to their platform?
@mikekistler @jeffhandley Do you have any thoughts on this PR?
|
On top-level docs: On CI (no checks running): |
|
The Build and Test / build (windows-latest, Debug) failure seems like a transient CI infrastructure issue. The log shows: All tests still ran and passed despite the timeout - 0 failures across all assemblies (1922 passed in ModelContextProtocol.Tests, 336 passed in ModelContextProtocol.AspNetCore.Tests, 57 passed in Analyzers.Tests). The @halter73 Could you please re-run the failed Windows Debug job? Nothing in our code changes would cause this. |
|
This PR adds 8 public types here (this static class with 4 URN constants + 3 methods, the provider, its options, the assertion context, the exception, and three layer-2 option DTOs). For comparison:
I think we should try to reduce, so we can get close to Go. Concrete cuts:
Lands us at 3 required public types ( Trade-off: the current "bring your own JAG" extension point goes away unless we re-expose helpers later. Given no one is asking for that today and we can promote back to |
halter73
left a comment
There was a problem hiding this comment.
Thanks for sticking with this! Two things before merge:
- A couple of earlier review threads were marked "Done" but weren't actually finished:
- One top-level class per file -
EnterpriseAuthProvider.csstill defines three top-level public types (EnterpriseAuthAssertionContext,EnterpriseAuthProvider,EnterpriseAuthProviderOptions), andEnterpriseAuth.csstill defines three internal response classes alongside the static helper. See inline comments for the splits. - Remove SEP-990 references — still present in
EnterpriseAuth.cs:51,McpJsonUtilities.cs:190, andREADME.mdlines 34/36. Inline comments below.
- Naming + public surface area.. Picking up the thread I left at #1305 (comment), I'd like to land on
CrossApplicationAccess*(TS-aligned, no abbreviations) and significantly shrink the public surface to match the Go SDK's 2-type shape. Details inline onEnterpriseAuth.cs.
Also withdrawing my earlier "consider this authentication, not authorization" aside — see the inline comment for why.
Requesting changes on the naming + visibility decision so we don't ship a public API we can't easily walk back.
…old IdP config into options
dc12435 to
db67570
Compare
|
Hey @halter73, I've addressed all of your review feedback. Here's a summary of what changed: Naming / file structure
Public surface reduction
Callback simplification
SEP-990 / spec reference cleanup
Context type
Let me know if anything else needs adjusting! |
There was a problem hiding this comment.
Thanks for the rename and the file split. The public surface (CrossApplicationAccess*) reads much better now.
That said, the PR has grown rather than shrunk in this round, and most of the new weight is testing internals rather than behavior. Before another pass I'd like to see the surface area pulled back and a few correctness/lifecycle issues addressed. Grouping the asks below so it's easier to triage:
Please remove
InternalsVisibleTofor both test assemblies inModelContextProtocol.Core.csproj. We intentionally do not use IVT in this repo (see #6 — it was one of the first things we removed after migrating to the modelcontextprotocol org). If a test can't be expressed against the public API, just delete it.- The nested
Throwhelper class inCrossApplicationAccess.cs.src/Common/Throw.csis already linked into Core (<Compile Include="..\Common\Throw.cs" Link="Throw.cs" />) and providesThrow.IfNull(with[NotNull]+CallerArgumentExpression) andThrow.IfNullOrWhiteSpace. Use those everywhere instead — including replacing the manualstring.IsNullOrEmpty(...) → throw new ArgumentException(...)blocks in theCrossApplicationAccessProviderconstructor. - The unused
DiscoverAndRequestJwtAuthorizationGrantAsyncoverload (and itsDiscoverAndRequestJwtAuthGrantOptionsDTO, and the 4 unit tests targeting it). WithCrossApplicationAccessnowinternal, that method has no callers — the production flow goes throughCrossApplicationAccessProvider.ResolveIdpTokenEndpointAsyncinstead. Once IVT is gone, the corresponding tests must go too. - The leftover "Enterprise" wording in
README.md(heading on line 34) and theasynclambda on line 57 that doesn'tawait.
Please change
HttpClientshould be a required, non-nullable constructor parameter onCrossApplicationAccessProvider, notHttpClient? httpClient = nullwith a?? new HttpClient()fallback. The owning caller (e.g.HttpClientTransport) is responsible for theHttpClientlifetime — this is exactly the patternClientOAuthProvideruses (seeClientOAuthProvider.cs:62). Apply the same fix to the internalCrossApplicationAccesshelpers that also dohttpClient ?? new HttpClient().- Don't echo raw HTTP response bodies into
CrossApplicationAccessException.Message. Keep the exception message to a short, sanitized summary (status code + endpoint). - The new docs roughly double the README size. Please move the bulk of it under docs/ (see the inline suggestion on line 65 for where).
Out of scope for this PR
The cache fields on CrossApplicationAccessProvider aren't thread-safe — but neither are ClientOAuthProvider._tokenCache / _authServerMetadata, nor the equivalents in the TS (CrossAppAccessProvider._tokens) and Go (EnterpriseHandler.tokenSource) SDKs. Since this is a pre-existing gap shared across providers (and SDKs) rather than something this PR introduces, I'd rather address it as a single follow-up that covers both C# providers and aligns with whatever TS/Go land on. Tracked in #1595 — no action needed in this PR.
Once the surface is trimmed and the HttpClient lifecycle matches ClientOAuthProvider, this should be in good shape.
|
And I hate to circle back on the naming, but I don't love But now I notice that Go went with A couple of options to consider, in rough order of my preference:
I do see that Edit: To be clear, I'm not going to hold up the PR on this. I'll merge with the current naming if we can't agree on something better, but I do think we can come up with something better. |
…st overload, move docs to transports.md
|
@halter73 Thanks for the follow-up on naming completely agree that
One thing worth flagging: from Okta's side, this feature is publicly marketed and documented as "Cross-Application Access" and "Enterprise Auth" those are the terms in our product docs, customer-facing guides, and related PRs across SDKs (Go, Python, TS). That's partly why those names surfaced here. So if discoverability for Okta customers integrating with MCP is a concern, CrossApplication* has real value developers following Okta docs will land on the right API immediately. That said, I understand the SDK should be vendor-neutral, and IdentityAssertionGrant* achieves that while staying spec-grounded. Happy to do the rename to IdentityAssertionGrant* in this PR if you agree it's a pure find-and-replace across the ~10 files we already split out. Or if you'd prefer to merge with CrossApplication* and rename in a follow-up, that works too. On the other review items addressed in the latest push:
|
Sounds good! Let's go with For the full rename, please also bring along:
Regarding Okta's marketing terms. That's a fair point. If you want to mention "Cross Application Access" or "Enterprise Auth" we can preserve discoverability in the XML doc comments and the docs page. We can even call out Okta. (e.g. "Implements Okta's Cross-Application Access / Identity Assertion Authorization Grant flow"). The type names themselves should track the spec. Thanks for going through all the back and forth on this. I think we'll be ready to merge after this final rename and fixing the netstandard build error. |
|
Please note that "Cross App Access" is not an Okta term, it's just that Okta has been doing a lot of press about it. So please don't call it "Okta's Cross App Access". We updated the spec to be more explicit about that name and it now is referenced in the abstract. https://www.ietf.org/archive/id/draft-ietf-oauth-identity-assertion-authz-grant-04.html |
|
Done! Here's what was addressed:
|
halter73
left a comment
There was a problem hiding this comment.
I filed #1617 and #1618 as follow ups. I'm good to merge it if you are @aniket-okta. Just let me know if I'm right that IDAG should be ID-JAG in transports.md.
|
Fixed — changed |
|
Thanks for addressing the multiple rounds of feedback. Much appreciated! |
Implements Enterprise Managed Authorization (SEP-990) for the C# MCP SDK, enabling MCP Clients to leverage enterprise Identity Providers for seamless authorization without per-server user authentication.
Closes #949
Flow
Design
Layer 2:
EnterpriseAuthstatic class : standalone utilities (~680 lines)RequestJwtAuthorizationGrantAsync(): RFC 8693 token exchange (ID Token → ID-JAG)DiscoverAndRequestJwtAuthorizationGrantAsync(): convenience wrapper with IdP discoveryExchangeJwtBearerGrantAsync(): RFC 7523 JWT bearer grant (ID-JAG → Access Token)DiscoverAuthServerMetadataAsync(): OAuth authorization server metadata discoveryRequestJwtAuthGrantOptions,DiscoverAndRequestJwtAuthGrantOptions,ExchangeJwtBearerGrantOptionsJagTokenExchangeResponse,JwtBearerAccessTokenResponse,OAuthErrorResponseLayer 3:
EnterpriseAuthProvider: high-level provider with caching (~230 lines)InvalidateCache()EnterpriseAuthProviderOptionsfor configuration (ClientId, ClientSecret, Scope, AssertionCallback)Tests
36 unit tests covering both layers, passing on net8.0, net9.0, and net10.0.
Related