MCP enterprise-managed authorization (client) - #1730
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-cloud | a718ae1 | Aug 26 2026, 09:58 PM |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
executor-marketing | a718ae1 | Commit Preview URL Branch Preview URL |
Aug 26 2026, 09:57 PM |
Contributor
Cloudflare previewTorn down — the PR is closed. |
@executor-js/cli
@executor-js/config
@executor-js/execution
@executor-js/sdk
@executor-js/codemode-core
@executor-js/runtime-quickjs
@executor-js/plugin-file-secrets
@executor-js/plugin-graphql
@executor-js/plugin-keychain
@executor-js/plugin-mcp
@executor-js/plugin-onepassword
@executor-js/plugin-openapi
executor
commit: |
…to connect An MCP oauth2 method may now name the registered OAuth app that plays its enterprise identity provider. The catalog projects that pointer so a client knows which app to name on oauth.start, and the start handler forwards the enterprise inputs it was already accepting. Declaring a provider only asks the connect path to try the ID-JAG grant; the server still has to advertise the profile, so an ordinary server keeps the interactive flow.
The refresh path switched on _tag by hand and the fallback rule lived in an exported predicate nothing called. Use catchTags for the mapping, assert the tags directly in the tests, and drop the predicate: the connect path's single catchTag is where that rule is actually enforced.
…tors A selfhost scenario runs the whole ID-JAG chain through the product: Okta issues the ID token, executor exchanges it for an ID-JAG and redeems it at the MCP server, and a tool call rides the result with no consent step. Seeding one DENY policy then proves a refusal surfaces as blocked-by-admin and does not fall back to the interactive flow. Both emulator ledgers carry the assertions. Needs emulate 0.14.0 for the Okta token exchange and its policy table.
The ID-JAG exchange had its own error-body machinery beside the existing
one, and it only understood a conform RFC 6749 envelope. An IdP answering
{"errors":["invalid_grant - ..."]} therefore read as a transport failure
and was retried forever instead of asking for a fresh sign-on.
Route it through toOAuth2ErrorWithHttpSummary, which now tries the conform
envelope first and falls back to the closed-set non-conform recovery. The
recovery only runs on 4xx, so a 5xx carrying an error code stays a
retryable transport verdict rather than becoming a permanent refusal.
Also hoist the body-reading helpers: a body-read failure on the exchange
response now fails distinctly instead of rendering as "did not match RFC
8693 2.2.1", and the read is passed as a thunk so a clone() on a consumed
body is caught rather than escaping as a defect.
A start failure carried only prose, so a console could not tell an
administrator refusal from a credential problem — and the two demand
opposite behaviour: blocked-by-admin must NOT offer the interactive
per-server flow, because that walks the user around the policy the
identity provider just enforced. Add blockedByAdmin and oauthErrorCode
alongside the pattern OAuthCompleteError already sets with
restartRequired.
Same file, second concern: { client, clientOwner } was written three times
(SDK interface, API Schema.Struct, MCP plugin Schema.Struct) and the
enterprise connect input twice. Each is now one Schema in the SDK that the
API and the MCP plugin reference.
SUBJECT_TOKEN_TYPES goes back to module-local: it is only ever an argument
to Schema.Literals.
…efresh The connect boundary flattened every EMA failure to a message string, so the taxonomy the module argues for at length stopped at the service edge. Translate per tag instead: a policy denial reaches the caller as blockedByAdmin with the identity providers own error code. Split the error union so minting cannot claim a verdict it never reaches. mintEnterpriseManagedAccessToken never inspects metadata, so EmaGrantProfileUnsupported belongs only to the discovery entry point; the refresh path loses its unreachable arm. Also: the connect paths authorization-server discovery was unbounded, because it was copied from scope discovery without the timeout. Both now share one capped, bounded probe loop. The inert Effect.provide on the EMA chain is gone — oauth4webapi drives the configured fetch, not HttpClient, and the layer read as a claim the chain did not honour.
Substring-matching the rendered prose let the tests pass on any error that happened to say the right thing, and left blockedByAdmin with no coverage at all. Assert the tag and the fields instead, at the unit, lifecycle, and e2e tiers. Cover the case that only exists over time: an administrator withdrawing access AFTER a connection was made, which the credential-refresh path meets with no user present. That is the only producer of blockedByAdmin on a credential failure, and it was untested. The OAuth fixture grows a policy control for it, since a denial that is fixed at construction cannot express the change. Decode the fixtures metadata through the production schema, so a decoder that stopped retaining authorization_grant_profiles_supported would fail the gate that reads it.
RhysSullivan
force-pushed
the
ema-client-auth
branch
from
August 26, 2026 21:54
13a50c8 to
a718ae1
Compare
RhysSullivan
marked this pull request as ready for review
August 26, 2026 22:03
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds client-side support for the MCP Enterprise-Managed Authorization extension, so a user already signed in to their enterprise IdP connects an MCP server with no per-server consent step.
The flow, per the EMA profile of draft-ietf-oauth-identity-assertion-authz-grant: the identity assertion from SSO is exchanged at the IdP for an ID-JAG (RFC 8693), which is redeemed at the MCP server's authorization server for an audience-restricted access token (RFC 7523). Servers opt in by advertising the id-jag grant profile in their RFC 8414 metadata; executor only takes this path when an enterprise IdP is configured for the server AND discovery confirms the profile.
Design decisions
Testing
Two tiers. Hermetic conformance tests drive the protocol against fixtures written to be stricter than any real server (assertion typ, signature, exact audience, client binding, expiry; an ID-JAG offered as a bearer token is rejected), covering the fallback, the three expiry tiers, confused-deputy, and scope narrowing.
The e2e scenario runs real product code against two emulators, Okta as the enterprise IdP and the MCP emulator as resource authorization server and MCP server, and asserts on both request ledgers rather than on executor's own responses. Phase one proves the connection completes with no authorize redirect and the tool call rides the chain-minted token. Phase two seeds an administrator DENY policy and proves the MCP server saw no authorize request, no registration, and no second redemption. Requires emulate 0.14.0.
Follow-ups