feat: Add Enterprise Managed Authorization (SEP-990) support#607
feat: Add Enterprise Managed Authorization (SEP-990) support#607prachi-okta wants to merge 5 commits intomodelcontextprotocol:mainfrom
Conversation
kpavlov
left a comment
There was a problem hiding this comment.
Thank you, @prachi-okta
I see this plugin is pretty generic and does not have any MCP-specifics. Not sure if it should belong to this project.
Did you consider submitting a PR to the Ktor project instead?
cc: @e5l
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |
Thanks for the thoughtful question! While EnterpriseAuthProvider uses Ktor's plugin conventions, the substance is entirely MCP/SEP-990 specific. Here's the exact flow it implements: ① Discovery (RFC 8414) ② JAG acquisition (RFC 8693) — delegated to assertionCallback ③ Access token exchange (RFC 7523) ④ Authenticated MCP request Steps ①③④ are handled internally by EnterpriseAuthProvider; step ② is delegated to the assertionCallback so callers can plug in their own IdP client. The token is cached with a configurable expiry buffer for proactive refresh. None of this — ID-JAG token types, RFC 9728 resource discovery, the assertion callback contract — belongs in a general-purpose Ktor plugin. It's MCP enterprise auth logic all the way down, which is why it lives here. This also mirrors how the TypeScript SDK handled it: CrossAppAccessProvider lives in @modelcontextprotocol/client (merged in typescript-sdk#1593 just a few days ago) for the same reasons. Happy to discuss further if you'd like! 😊 |
devcrocod
left a comment
There was a problem hiding this comment.
Auth should not come bundled with the client. It should be optional, which is also explicitly stated in the specification
I have not reviewed the entire PR because I already have concerns about introducing this in the first place. As I understand it, this is an auth extension, while Authorization itself is described in the specification as optional
Authorization is OPTIONAL for MCP implementations
from https://modelcontextprotocol.io/specification/2025-11-25/basic/authorization#protocol-requirements
Even if we decide to support Authorization and its extensions, I think the implementation and API should be designed more carefully. For example, the current impl use of data classes can leak secrets through the auto-generated toString()
Implements Enterprise Managed Authorization (SEP-990) for the Kotlin MCP SDK
Enables MCP clients to leverage enterprise Identity Providers for seamless authorization without per-server user authentication.
Motivation and Context
Enterprise environments require OAuth flows where users authenticate with a centralized IdP, and applications need to securely access protected MCP resources on their behalf. SEP-990 addresses this via:
This follows the same provider pattern as existing auth implementations and is consistent with the Java, TypeScript, C#, and Go SDK implementations. The provider is implemented as a Ktor
HttpClientPluginthat intercepts outgoing requests viaHttpSend, making it a first-class citizen of the Ktor client ecosystem.How Has This Been Tested?
Added 32 unit tests across two test classes using Kotlin Multiplatform (
commonTest), Kotest assertions, and KtorMockEngine:EnterpriseAuthTest(20 tests):oauth-authorization-server, fallback toopenid-configurationon 404/500, both-fail error, trailing slash strippingissued_token_typethrows, non-standardtoken_typesucceeds (informational per RFC 8693 §2.2.1), missingaccess_tokenthrows, HTTP error throwsdiscoverAndRequestJwtAuthorizationGrant— full discovery flow, skips discovery whenidpTokenEndpointprovided, notoken_endpointin metadata throwsexpiresAtcomputed from monotonic clock, missingaccess_tokenthrows, HTTP error throwsisExpired— nullexpiresAt, futureexpiresAt, pastexpiresAtEnterpriseAuthProviderTest(12 tests):Authorization: Bearerheader injection via Ktor plugininvalidateCacheforces re-fetch on next requestresourceUrlandauthorizationServerUrlpassed to callbackpreparevalidation —clientIdnull throws,assertionCallbacknull throwsIllegalArgumentExceptionexpires_incached indefinitelyexpiryBufferrespectedBreaking Changes
None — this is a purely additive feature. No existing APIs are modified.
Types of changes
Checklist
./gradlew :kotlin-sdk-client:jvmTest)./gradlew apiCheckpassesAdditional context
Key design decisions:
EnterpriseAuthProviderimplementsHttpClientPlugin<Config, EnterpriseAuthProvider>, installed viainstall(EnterpriseAuthProvider) { ... }DSL; usesHttpSendinterceptor for transparent header injectionclient_secret_basic(Basic Auth header) used for JWT bearer grant, aligned with SEP-990 conformance requirements (RFC 6749 §2.3.1)token_typein JAG response is not strictly validated per RFC 8693 §2.2.1 — it is informational and strict checking rejects conformant IdPsTimeSource.Monotonic) used for token expiry to avoid wall-clock skew issuesMutexfor thread-safe token caching on coroutinesRelated SDK implementations: