From cbe47689be290b89d737b142b16fe0367f24ec2f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:26:46 +0000 Subject: [PATCH 1/5] Update stacklok/toolhive to v0.44.0 Signed-off-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/upstream-projects.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/upstream-projects.yaml b/.github/upstream-projects.yaml index 0673e75c..03ac27b1 100644 --- a/.github/upstream-projects.yaml +++ b/.github/upstream-projects.yaml @@ -44,7 +44,7 @@ projects: - id: toolhive repo: stacklok/toolhive - version: v0.43.0 + version: v0.44.0 # toolhive is a monorepo covering the CLI, the Kubernetes # operator, and the vMCP gateway. It also introduces cross- # cutting features that land in concepts/, integrations/, From f4ca0fe337d226fd5e1e345e9fb9fbb8f6ce693f Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 24 Aug 2026 19:27:43 +0000 Subject: [PATCH 2/5] Refresh reference assets for toolhive v0.44.0 --- static/api-specs/toolhive-api.yaml | 16 +++- .../mcpexternalauthconfigs.schema.json | 93 +++++++++++++++++++ .../virtualmcpservers.schema.json | 93 +++++++++++++++++++ 3 files changed, 198 insertions(+), 4 deletions(-) diff --git a/static/api-specs/toolhive-api.yaml b/static/api-specs/toolhive-api.yaml index fde91175..161320f0 100644 --- a/static/api-specs/toolhive-api.yaml +++ b/static/api-specs/toolhive-api.yaml @@ -976,6 +976,14 @@ components: instead of Extra (assignClaim routes it to that field) — it is still the external token's client_id claim, not a ToolHive one. type: string + allow_may_act: + description: |- + AllowMayAct permits this external issuer's may_act claim to authorize + delegation. It defaults to false; external issuers must be opted in + explicitly because may_act bypasses AllowedActors. It does not affect + self-issued subject tokens. When enabled, AllowedDelegateClients must + name specific ToolHive clients rather than use the wildcard. + type: boolean allow_private_ips: description: |- AllowPrivateIPs permits OIDC discovery and JWKS fetches for THIS @@ -986,10 +994,10 @@ components: description: |- AllowedActors is the allowlist of ActorClaim values authorized to exchange a subject token from this issuer when it carries no - "may_act" claim; empty means only may_act-bearing tokens are - accepted. By itself names no ToolHive client — see - AllowedDelegateClients and docs/arch/17-token-exchange-delegation.md - ("Accepted limitations" #1). + "may_act" claim. Empty denies every token unless AllowMayAct is true + and the token carries a permitted may_act claim. By itself names no + ToolHive client — see AllowedDelegateClients and + docs/arch/17-token-exchange-delegation.md ("Accepted limitations" #1). items: type: string type: array diff --git a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json index da0c4268..c8a44e5d 100644 --- a/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json +++ b/static/api-specs/toolhive-crds/mcpexternalauthconfigs.schema.json @@ -560,6 +560,99 @@ }, "type": "object" }, + "trustedIssuers": { + "description": "TrustedIssuers configures external OIDC issuers whose tokens are\naccepted as RFC 8693 subject tokens during token exchange, in addition\nto self-issued subject tokens. Empty (the default) means only\nself-issued subject tokens are accepted. See\ndocs/arch/17-token-exchange-delegation.md for the trust model.", + "items": { + "description": "TrustedIssuerConfig configures an external OIDC issuer whose tokens are\naccepted as RFC 8693 subject tokens during token exchange. It mirrors\ntokenexchange.TrustedIssuer (pkg/authserver/server/tokenexchange), the\nruntime type the operator converts this into directly — no secret is\nreferenced by this type, so no SecretKeyRef indirection is needed, unlike\nDelegateClientConfig.", + "properties": { + "actorClaim": { + "description": "ActorClaim names the claim identifying the client that requested the\nsubject token from this external issuer (used by allowedActors below).\nDefaults to \"azp\" when empty; use \"appid\" for Microsoft Entra v1, \"cid\"\nfor Okta. The special value \"client_id\" reads the subject token's\nclient_id claim instead.", + "maxLength": 64, + "type": "string" + }, + "allowMayAct": { + "default": false, + "description": "AllowMayAct permits this external issuer's may_act claim to authorize\ndelegation. Defaults to false; external issuers must be opted in\nexplicitly because may_act bypasses allowedActors. Does not affect\nself-issued subject tokens. The wildcard is never permitted alongside\nspecific allowedDelegateClients, regardless of this setting.", + "type": "boolean" + }, + "allowPrivateIPs": { + "description": "AllowPrivateIPs permits OIDC discovery and JWKS fetches for THIS issuer\nto resolve to a private or loopback address. Use only when the issuer\nis hosted inside the same cluster and has no public endpoint. Requires\njwksUrl to be set explicitly (enforced at reconcile time), since\notherwise OIDC discovery — fetched from the external issuer itself —\nwould choose the private dial target.", + "type": "boolean" + }, + "allowedActors": { + "description": "AllowedActors is the allowlist of actorClaim values authorized to\nexchange a subject token from this issuer when it carries no\n\"may_act\" claim. Empty denies every token unless allowMayAct is true\nand the token carries a permitted may_act claim.", + "items": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "maxItems": 50, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "allowedDelegateClients": { + "description": "AllowedDelegateClients restricts which ToolHive client IDs may\nexchange a subject token from this issuer. Required; set it to [\"*\"]\nto permit any confidential client holding the token-exchange grant. The\nwildcard must be the only entry; otherwise list specific client IDs to\nbind delegation to them.", + "items": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "expectedAudience": { + "description": "ExpectedAudience is the expected \"aud\" claim value that must appear in\nthe token's audience list. This should be a resource/API identifier\n(e.g. a URI), not a client ID.", + "maxLength": 2048, + "minLength": 1, + "type": "string" + }, + "insecureAllowHTTP": { + "description": "InsecureAllowHTTP permits plain-HTTP OIDC discovery and JWKS fetches\nfor THIS issuer only. Development and testing only — never set in\nproduction.", + "type": "boolean" + }, + "issuerUrl": { + "description": "IssuerURL is the expected \"iss\" claim value (exact match).", + "maxLength": 2048, + "minLength": 1, + "type": "string" + }, + "jwksUrl": { + "description": "JWKSURL is the URL to fetch the issuer's JSON Web Key Set from. If\nempty, it is resolved via OIDC discovery at\n{issuerUrl}/.well-known/openid-configuration.", + "maxLength": 2048, + "type": "string" + } + }, + "required": [ + "allowedDelegateClients", + "expectedAudience", + "issuerUrl" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "allowedDelegateClients must not combine the wildcard \"*\" with specific client IDs", + "rule": "!('*' in self.allowedDelegateClients) || size(self.allowedDelegateClients) == 1" + }, + { + "message": "allowMayAct must not be enabled when allowedDelegateClients contains the wildcard \"*\"", + "rule": "!(has(self.allowMayAct) && self.allowMayAct && '*' in self.allowedDelegateClients)" + }, + { + "message": "actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid", + "rule": "!has(self.actorClaim) || !(self.actorClaim in ['sub', 'iss', 'aud', 'exp', 'iat', 'nbf', 'jti', 'name', 'email', 'scope', 'scp', 'may_act'])" + }, + { + "message": "allowPrivateIPs requires jwksUrl to be set explicitly", + "rule": "!(has(self.allowPrivateIPs) && self.allowPrivateIPs) || (has(self.jwksUrl) && self.jwksUrl != \"\")" + } + ] + }, + "maxItems": 20, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, "upstreamProviders": { "description": "UpstreamProviders configures connections to upstream Identity Providers.\nThe embedded auth server delegates authentication to these providers.\nMCPServer and MCPRemoteProxy support a single upstream; VirtualMCPServer supports multiple.", "items": { diff --git a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json index 049977ab..c106ef4f 100644 --- a/static/api-specs/toolhive-crds/virtualmcpservers.schema.json +++ b/static/api-specs/toolhive-crds/virtualmcpservers.schema.json @@ -451,6 +451,99 @@ }, "type": "object" }, + "trustedIssuers": { + "description": "TrustedIssuers configures external OIDC issuers whose tokens are\naccepted as RFC 8693 subject tokens during token exchange, in addition\nto self-issued subject tokens. Empty (the default) means only\nself-issued subject tokens are accepted. See\ndocs/arch/17-token-exchange-delegation.md for the trust model.", + "items": { + "description": "TrustedIssuerConfig configures an external OIDC issuer whose tokens are\naccepted as RFC 8693 subject tokens during token exchange. It mirrors\ntokenexchange.TrustedIssuer (pkg/authserver/server/tokenexchange), the\nruntime type the operator converts this into directly — no secret is\nreferenced by this type, so no SecretKeyRef indirection is needed, unlike\nDelegateClientConfig.", + "properties": { + "actorClaim": { + "description": "ActorClaim names the claim identifying the client that requested the\nsubject token from this external issuer (used by allowedActors below).\nDefaults to \"azp\" when empty; use \"appid\" for Microsoft Entra v1, \"cid\"\nfor Okta. The special value \"client_id\" reads the subject token's\nclient_id claim instead.", + "maxLength": 64, + "type": "string" + }, + "allowMayAct": { + "default": false, + "description": "AllowMayAct permits this external issuer's may_act claim to authorize\ndelegation. Defaults to false; external issuers must be opted in\nexplicitly because may_act bypasses allowedActors. Does not affect\nself-issued subject tokens. The wildcard is never permitted alongside\nspecific allowedDelegateClients, regardless of this setting.", + "type": "boolean" + }, + "allowPrivateIPs": { + "description": "AllowPrivateIPs permits OIDC discovery and JWKS fetches for THIS issuer\nto resolve to a private or loopback address. Use only when the issuer\nis hosted inside the same cluster and has no public endpoint. Requires\njwksUrl to be set explicitly (enforced at reconcile time), since\notherwise OIDC discovery — fetched from the external issuer itself —\nwould choose the private dial target.", + "type": "boolean" + }, + "allowedActors": { + "description": "AllowedActors is the allowlist of actorClaim values authorized to\nexchange a subject token from this issuer when it carries no\n\"may_act\" claim. Empty denies every token unless allowMayAct is true\nand the token carries a permitted may_act claim.", + "items": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "maxItems": 50, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "allowedDelegateClients": { + "description": "AllowedDelegateClients restricts which ToolHive client IDs may\nexchange a subject token from this issuer. Required; set it to [\"*\"]\nto permit any confidential client holding the token-exchange grant. The\nwildcard must be the only entry; otherwise list specific client IDs to\nbind delegation to them.", + "items": { + "maxLength": 256, + "minLength": 1, + "type": "string" + }, + "maxItems": 50, + "minItems": 1, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, + "expectedAudience": { + "description": "ExpectedAudience is the expected \"aud\" claim value that must appear in\nthe token's audience list. This should be a resource/API identifier\n(e.g. a URI), not a client ID.", + "maxLength": 2048, + "minLength": 1, + "type": "string" + }, + "insecureAllowHTTP": { + "description": "InsecureAllowHTTP permits plain-HTTP OIDC discovery and JWKS fetches\nfor THIS issuer only. Development and testing only — never set in\nproduction.", + "type": "boolean" + }, + "issuerUrl": { + "description": "IssuerURL is the expected \"iss\" claim value (exact match).", + "maxLength": 2048, + "minLength": 1, + "type": "string" + }, + "jwksUrl": { + "description": "JWKSURL is the URL to fetch the issuer's JSON Web Key Set from. If\nempty, it is resolved via OIDC discovery at\n{issuerUrl}/.well-known/openid-configuration.", + "maxLength": 2048, + "type": "string" + } + }, + "required": [ + "allowedDelegateClients", + "expectedAudience", + "issuerUrl" + ], + "type": "object", + "x-kubernetes-validations": [ + { + "message": "allowedDelegateClients must not combine the wildcard \"*\" with specific client IDs", + "rule": "!('*' in self.allowedDelegateClients) || size(self.allowedDelegateClients) == 1" + }, + { + "message": "allowMayAct must not be enabled when allowedDelegateClients contains the wildcard \"*\"", + "rule": "!(has(self.allowMayAct) && self.allowMayAct && '*' in self.allowedDelegateClients)" + }, + { + "message": "actorClaim must name a readable claim; use client_id or a non-reserved claim such as azp, appid, or cid", + "rule": "!has(self.actorClaim) || !(self.actorClaim in ['sub', 'iss', 'aud', 'exp', 'iat', 'nbf', 'jti', 'name', 'email', 'scope', 'scp', 'may_act'])" + }, + { + "message": "allowPrivateIPs requires jwksUrl to be set explicitly", + "rule": "!(has(self.allowPrivateIPs) && self.allowPrivateIPs) || (has(self.jwksUrl) && self.jwksUrl != \"\")" + } + ] + }, + "maxItems": 20, + "type": "array", + "x-kubernetes-list-type": "atomic" + }, "upstreamProviders": { "description": "UpstreamProviders configures connections to upstream Identity Providers.\nThe embedded auth server delegates authentication to these providers.\nMCPServer and MCPRemoteProxy support a single upstream; VirtualMCPServer supports multiple.", "items": { From 44d514648053a65169da8dc7cda6340cb1e2247a Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:39:20 +0000 Subject: [PATCH 3/5] Document trustedIssuers for embedded auth server Add an "Accept subject tokens from external issuers" section to both the vMCP and Kubernetes embedded auth server guides, covering the new `trustedIssuers` CRD field shipped in toolhive v0.44.0 (stacklok/toolhive#6353) that lets the RFC 8693 token-exchange grant accept subject tokens minted by an external OIDC issuer. Co-Authored-By: Claude Opus 4.7 --- .../guides-k8s/embedded-auth-server-k8s.mdx | 57 +++++++++++++++++++ .../guides-vmcp/embedded-auth-server-vmcp.mdx | 35 ++++++++++++ 2 files changed, 92 insertions(+) diff --git a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx index 04c72a30..6ae7be8f 100644 --- a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx +++ b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx @@ -536,6 +536,63 @@ endpoints. The delegate-clients validation blocks a plaintext `http://` issuer categorically at admission. The CEL admission rule cannot express the loopback exception, so use an `https://` issuer whenever you configure delegate clients. +### Accept subject tokens from external issuers + +By default, the token-exchange grant only accepts subject tokens that the +embedded auth server itself issued. To also accept tokens minted by an external +OIDC issuer (for example, a corporate IdP whose access tokens a client already +holds), add a `trustedIssuers` entry. A delegate client presenting an +externally-issued subject token can then exchange it for a ToolHive-scoped +delegated token without a separate ToolHive login. + +Each trusted issuer names the external IdP and states which of its clients may +present subject tokens, and which ToolHive delegate clients may exchange them: + +```yaml title="MCPExternalAuthConfig: trusted issuers" +spec: + embeddedAuthServer: + trustedIssuers: + - issuerUrl: https://login.example-idp.com + expectedAudience: https://mcp.example.com + allowedActors: + - external-reporting-client + allowedDelegateClients: + - reporting-delegate +``` + +The exchange still needs a delegate client entry, because RFC 8693 requires +authenticated access to `/oauth/token` and the delegate client is what +authenticates. `trustedIssuers` grants the delegate client permission to accept +a subject token from the external issuer; on its own it does not create a usable +exchange path. + +The fields are: + +| Field | Required | Description | +| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `issuerUrl` | Yes | Expected `iss` claim value on the subject token (exact match). | +| `expectedAudience` | Yes | Resource/API identifier that must appear in the token's `aud`. Not a client ID. | +| `allowedDelegateClients` | Yes | ToolHive delegate `clientId`s permitted to exchange this issuer's tokens. Use `["*"]` to permit any confidential client with the token-exchange grant. | +| `allowedActors` | No | External client IDs whose tokens may be exchanged. Read from the claim named by `actorClaim`. Empty denies every token unless `allowMayAct` authorizes a specific delegation. | +| `actorClaim` | No | Claim on the subject token identifying the caller. Defaults to `azp`. Use `appid` for Microsoft Entra v1, `cid` for Okta, or the literal value `client_id` (a sentinel, not a claim name) to read the token's `client_id` claim. | +| `allowMayAct` | No | Set to `true` to honor a `may_act` claim from this issuer (RFC 8693 §4.4). Defaults to `false`; `may_act` bypasses `allowedActors`, so external issuers must opt in explicitly. Rejected when `allowedDelegateClients` is `["*"]`. | +| `jwksUrl` | No | JWKS endpoint for signature verification. When omitted, resolved via OIDC discovery at `{issuerUrl}/.well-known/openid-configuration`. | +| `allowPrivateIPs` | No | Permit OIDC discovery and JWKS fetches for this issuer to resolve to a private or loopback address. Requires `jwksUrl` to be set explicitly. | +| `insecureAllowHTTP` | No | Permit plain-HTTP OIDC discovery and JWKS fetches for this issuer. Development and testing only. | + +`allowedDelegateClients` is what binds an external actor to a specific ToolHive +delegate client. Without it, every confidential client holding the +token-exchange grant would be equivalent for delegation purposes. Setting +`allowedDelegateClients: ["*"]` explicitly declares that permissiveness; the +wildcard cannot be combined with specific client IDs, and cannot be combined +with `allowMayAct: true`. + +The delegated token's `sub` is qualified as `#` so that +subjects from different issuers cannot collide. The `act` claim records +provenance: the outer hop carries ToolHive's issuer and the authenticated client +ID, and the nested hop carries the external issuer and the allowed actor (when +one matched). + ### Enable baseline scopes for DCR clients Some MCP clients (for example, Claude Code) register via DCR with a narrowed diff --git a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx index 1a27d490..efa35706 100644 --- a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx +++ b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx @@ -354,6 +354,41 @@ All four fields are required. Delegate clients are independent of `allowConfidentialClientRegistration`. The issuer must use `https://`; delegate clients are rejected at admission when the issuer is plaintext HTTP. +### Accept subject tokens from external issuers + +By default, the token-exchange grant only accepts subject tokens the embedded +auth server itself issued. To also accept tokens minted by an external OIDC +issuer (for example, a corporate IdP), add a `trustedIssuers` entry alongside +your delegate client: + +```yaml title="VirtualMCPServer: trusted issuers" +spec: + authServerConfig: + trustedIssuers: + - issuerUrl: https://login.example-idp.com + expectedAudience: https://mcp.example.com + allowedActors: + - external-reporting-client + allowedDelegateClients: + - backend-exchange +``` + +`issuerUrl`, `expectedAudience`, and `allowedDelegateClients` are required. +`allowedDelegateClients` names the delegate clients permitted to exchange this +issuer's tokens, or `["*"]` to permit any confidential client holding the +token-exchange grant. The exchange still requires a matching entry under +`delegateClients` to authenticate at `/oauth/token`. + +`allowMayAct: true` opts this issuer's `may_act` claim in as a delegation +signal; it defaults to `false` because `may_act` bypasses `allowedActors`, so +you must opt each external issuer in explicitly. Enabling it is rejected when +`allowedDelegateClients` is `["*"]`. + +For the full field list (including `actorClaim`, `jwksUrl`, `allowPrivateIPs`, +and `insecureAllowHTTP`) and the trust model behind the two consent paths, see +[Accept subject tokens from external issuers](../guides-k8s/embedded-auth-server-k8s.mdx#accept-subject-tokens-from-external-issuers) +in the Kubernetes guide. The configuration structure is identical. + ### Enable baseline scopes for DCR clients If your MCP clients register via DCR with a narrowed `scope` value and then From 84f7ece6fb9eda336a19c6cff3024203d132b3c3 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <41898282+claude[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 19:43:00 +0000 Subject: [PATCH 4/5] Apply prettier and eslint fixups to skill output --- docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx index efa35706..ded60dda 100644 --- a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx +++ b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx @@ -379,13 +379,14 @@ issuer's tokens, or `["*"]` to permit any confidential client holding the token-exchange grant. The exchange still requires a matching entry under `delegateClients` to authenticate at `/oauth/token`. -`allowMayAct: true` opts this issuer's `may_act` claim in as a delegation -signal; it defaults to `false` because `may_act` bypasses `allowedActors`, so -you must opt each external issuer in explicitly. Enabling it is rejected when +Set `allowMayAct: true` to treat this issuer's `may_act` claim as a delegation +signal. It defaults to `false` because `may_act` bypasses `allowedActors`, so +you must opt in each external issuer explicitly. Enabling it is rejected when `allowedDelegateClients` is `["*"]`. For the full field list (including `actorClaim`, `jwksUrl`, `allowPrivateIPs`, -and `insecureAllowHTTP`) and the trust model behind the two consent paths, see +and `insecureAllowHTTP`) and how the delegated token's `sub` and `act` claims +are constructed, see [Accept subject tokens from external issuers](../guides-k8s/embedded-auth-server-k8s.mdx#accept-subject-tokens-from-external-issuers) in the Kubernetes guide. The configuration structure is identical. From 7d1844f3ef4b6070de09f6586b0aad640cec95ca Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Tue, 25 Aug 2026 13:28:44 +0200 Subject: [PATCH 5/5] Trim duplicated delegation guidance Replace the hand-written trustedIssuers sections with pointers to the dedicated token exchange guide. Keep generated CRD references as the single source for field details and avoid duplicating the conceptual delegation guidance. --- .../guides-k8s/embedded-auth-server-k8s.mdx | 57 ------------------- .../guides-vmcp/embedded-auth-server-vmcp.mdx | 36 ------------ 2 files changed, 93 deletions(-) diff --git a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx index 6ae7be8f..04c72a30 100644 --- a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx +++ b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx @@ -536,63 +536,6 @@ endpoints. The delegate-clients validation blocks a plaintext `http://` issuer categorically at admission. The CEL admission rule cannot express the loopback exception, so use an `https://` issuer whenever you configure delegate clients. -### Accept subject tokens from external issuers - -By default, the token-exchange grant only accepts subject tokens that the -embedded auth server itself issued. To also accept tokens minted by an external -OIDC issuer (for example, a corporate IdP whose access tokens a client already -holds), add a `trustedIssuers` entry. A delegate client presenting an -externally-issued subject token can then exchange it for a ToolHive-scoped -delegated token without a separate ToolHive login. - -Each trusted issuer names the external IdP and states which of its clients may -present subject tokens, and which ToolHive delegate clients may exchange them: - -```yaml title="MCPExternalAuthConfig: trusted issuers" -spec: - embeddedAuthServer: - trustedIssuers: - - issuerUrl: https://login.example-idp.com - expectedAudience: https://mcp.example.com - allowedActors: - - external-reporting-client - allowedDelegateClients: - - reporting-delegate -``` - -The exchange still needs a delegate client entry, because RFC 8693 requires -authenticated access to `/oauth/token` and the delegate client is what -authenticates. `trustedIssuers` grants the delegate client permission to accept -a subject token from the external issuer; on its own it does not create a usable -exchange path. - -The fields are: - -| Field | Required | Description | -| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `issuerUrl` | Yes | Expected `iss` claim value on the subject token (exact match). | -| `expectedAudience` | Yes | Resource/API identifier that must appear in the token's `aud`. Not a client ID. | -| `allowedDelegateClients` | Yes | ToolHive delegate `clientId`s permitted to exchange this issuer's tokens. Use `["*"]` to permit any confidential client with the token-exchange grant. | -| `allowedActors` | No | External client IDs whose tokens may be exchanged. Read from the claim named by `actorClaim`. Empty denies every token unless `allowMayAct` authorizes a specific delegation. | -| `actorClaim` | No | Claim on the subject token identifying the caller. Defaults to `azp`. Use `appid` for Microsoft Entra v1, `cid` for Okta, or the literal value `client_id` (a sentinel, not a claim name) to read the token's `client_id` claim. | -| `allowMayAct` | No | Set to `true` to honor a `may_act` claim from this issuer (RFC 8693 §4.4). Defaults to `false`; `may_act` bypasses `allowedActors`, so external issuers must opt in explicitly. Rejected when `allowedDelegateClients` is `["*"]`. | -| `jwksUrl` | No | JWKS endpoint for signature verification. When omitted, resolved via OIDC discovery at `{issuerUrl}/.well-known/openid-configuration`. | -| `allowPrivateIPs` | No | Permit OIDC discovery and JWKS fetches for this issuer to resolve to a private or loopback address. Requires `jwksUrl` to be set explicitly. | -| `insecureAllowHTTP` | No | Permit plain-HTTP OIDC discovery and JWKS fetches for this issuer. Development and testing only. | - -`allowedDelegateClients` is what binds an external actor to a specific ToolHive -delegate client. Without it, every confidential client holding the -token-exchange grant would be equivalent for delegation purposes. Setting -`allowedDelegateClients: ["*"]` explicitly declares that permissiveness; the -wildcard cannot be combined with specific client IDs, and cannot be combined -with `allowMayAct: true`. - -The delegated token's `sub` is qualified as `#` so that -subjects from different issuers cannot collide. The `act` claim records -provenance: the outer hop carries ToolHive's issuer and the authenticated client -ID, and the nested hop carries the external issuer and the allowed actor (when -one matched). - ### Enable baseline scopes for DCR clients Some MCP clients (for example, Claude Code) register via DCR with a narrowed diff --git a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx index ded60dda..1a27d490 100644 --- a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx +++ b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx @@ -354,42 +354,6 @@ All four fields are required. Delegate clients are independent of `allowConfidentialClientRegistration`. The issuer must use `https://`; delegate clients are rejected at admission when the issuer is plaintext HTTP. -### Accept subject tokens from external issuers - -By default, the token-exchange grant only accepts subject tokens the embedded -auth server itself issued. To also accept tokens minted by an external OIDC -issuer (for example, a corporate IdP), add a `trustedIssuers` entry alongside -your delegate client: - -```yaml title="VirtualMCPServer: trusted issuers" -spec: - authServerConfig: - trustedIssuers: - - issuerUrl: https://login.example-idp.com - expectedAudience: https://mcp.example.com - allowedActors: - - external-reporting-client - allowedDelegateClients: - - backend-exchange -``` - -`issuerUrl`, `expectedAudience`, and `allowedDelegateClients` are required. -`allowedDelegateClients` names the delegate clients permitted to exchange this -issuer's tokens, or `["*"]` to permit any confidential client holding the -token-exchange grant. The exchange still requires a matching entry under -`delegateClients` to authenticate at `/oauth/token`. - -Set `allowMayAct: true` to treat this issuer's `may_act` claim as a delegation -signal. It defaults to `false` because `may_act` bypasses `allowedActors`, so -you must opt in each external issuer explicitly. Enabling it is rejected when -`allowedDelegateClients` is `["*"]`. - -For the full field list (including `actorClaim`, `jwksUrl`, `allowPrivateIPs`, -and `insecureAllowHTTP`) and how the delegated token's `sub` and `act` claims -are constructed, see -[Accept subject tokens from external issuers](../guides-k8s/embedded-auth-server-k8s.mdx#accept-subject-tokens-from-external-issuers) -in the Kubernetes guide. The configuration structure is identical. - ### Enable baseline scopes for DCR clients If your MCP clients register via DCR with a narrowed `scope` value and then