Skip to content

Commit 07035d9

Browse files
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 <noreply@anthropic.com>
1 parent 99f3cd3 commit 07035d9

2 files changed

Lines changed: 92 additions & 0 deletions

File tree

docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,63 @@ endpoints. The delegate-clients validation blocks a plaintext `http://` issuer
536536
categorically at admission. The CEL admission rule cannot express the loopback
537537
exception, so use an `https://` issuer whenever you configure delegate clients.
538538

539+
### Accept subject tokens from external issuers
540+
541+
By default, the token-exchange grant only accepts subject tokens that the
542+
embedded auth server itself issued. To also accept tokens minted by an external
543+
OIDC issuer (for example, a corporate IdP whose access tokens a client already
544+
holds), add a `trustedIssuers` entry. A delegate client presenting an
545+
externally-issued subject token can then exchange it for a ToolHive-scoped
546+
delegated token without a separate ToolHive login.
547+
548+
Each trusted issuer names the external IdP and states which of its clients may
549+
present subject tokens, and which ToolHive delegate clients may exchange them:
550+
551+
```yaml title="MCPExternalAuthConfig: trusted issuers"
552+
spec:
553+
embeddedAuthServer:
554+
trustedIssuers:
555+
- issuerUrl: https://login.example-idp.com
556+
expectedAudience: https://mcp.example.com
557+
allowedActors:
558+
- external-reporting-client
559+
allowedDelegateClients:
560+
- reporting-delegate
561+
```
562+
563+
The exchange still needs a delegate client entry, because RFC 8693 requires
564+
authenticated access to `/oauth/token` and the delegate client is what
565+
authenticates. `trustedIssuers` grants the delegate client permission to accept
566+
a subject token from the external issuer; on its own it does not create a usable
567+
exchange path.
568+
569+
The fields are:
570+
571+
| Field | Required | Description |
572+
| ------------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
573+
| `issuerUrl` | Yes | Expected `iss` claim value on the subject token (exact match). |
574+
| `expectedAudience` | Yes | Resource/API identifier that must appear in the token's `aud`. Not a client ID. |
575+
| `allowedDelegateClients` | Yes | ToolHive delegate `clientId`s permitted to exchange this issuer's tokens. Use `["*"]` to permit any confidential client with the token-exchange grant. |
576+
| `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. |
577+
| `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. |
578+
| `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 `["*"]`. |
579+
| `jwksUrl` | No | JWKS endpoint for signature verification. When omitted, resolved via OIDC discovery at `{issuerUrl}/.well-known/openid-configuration`. |
580+
| `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. |
581+
| `insecureAllowHTTP` | No | Permit plain-HTTP OIDC discovery and JWKS fetches for this issuer. Development and testing only. |
582+
583+
`allowedDelegateClients` is what binds an external actor to a specific ToolHive
584+
delegate client. Without it, every confidential client holding the
585+
token-exchange grant would be equivalent for delegation purposes. Setting
586+
`allowedDelegateClients: ["*"]` explicitly declares that permissiveness; the
587+
wildcard cannot be combined with specific client IDs, and cannot be combined
588+
with `allowMayAct: true`.
589+
590+
The delegated token's `sub` is qualified as `<issuerUrl>#<externalSub>` so that
591+
subjects from different issuers cannot collide. The `act` claim records
592+
provenance: the outer hop carries ToolHive's issuer and the authenticated client
593+
ID, and the nested hop carries the external issuer and the allowed actor (when
594+
one matched).
595+
539596
### Enable baseline scopes for DCR clients
540597

541598
Some MCP clients (for example, Claude Code) register via DCR with a narrowed

docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,41 @@ All four fields are required. Delegate clients are independent of
354354
`allowConfidentialClientRegistration`. The issuer must use `https://`; delegate
355355
clients are rejected at admission when the issuer is plaintext HTTP.
356356

357+
### Accept subject tokens from external issuers
358+
359+
By default, the token-exchange grant only accepts subject tokens the embedded
360+
auth server itself issued. To also accept tokens minted by an external OIDC
361+
issuer (for example, a corporate IdP), add a `trustedIssuers` entry alongside
362+
your delegate client:
363+
364+
```yaml title="VirtualMCPServer: trusted issuers"
365+
spec:
366+
authServerConfig:
367+
trustedIssuers:
368+
- issuerUrl: https://login.example-idp.com
369+
expectedAudience: https://mcp.example.com
370+
allowedActors:
371+
- external-reporting-client
372+
allowedDelegateClients:
373+
- backend-exchange
374+
```
375+
376+
`issuerUrl`, `expectedAudience`, and `allowedDelegateClients` are required.
377+
`allowedDelegateClients` names the delegate clients permitted to exchange this
378+
issuer's tokens, or `["*"]` to permit any confidential client holding the
379+
token-exchange grant. The exchange still requires a matching entry under
380+
`delegateClients` to authenticate at `/oauth/token`.
381+
382+
`allowMayAct: true` opts this issuer's `may_act` claim in as a delegation
383+
signal; it defaults to `false` because `may_act` bypasses `allowedActors`, so
384+
you must opt each external issuer in explicitly. Enabling it is rejected when
385+
`allowedDelegateClients` is `["*"]`.
386+
387+
For the full field list (including `actorClaim`, `jwksUrl`, `allowPrivateIPs`,
388+
and `insecureAllowHTTP`) and the trust model behind the two consent paths, see
389+
[Accept subject tokens from external issuers](../guides-k8s/embedded-auth-server-k8s.mdx#accept-subject-tokens-from-external-issuers)
390+
in the Kubernetes guide. The configuration structure is identical.
391+
357392
### Enable baseline scopes for DCR clients
358393

359394
If your MCP clients register via DCR with a narrowed `scope` value and then

0 commit comments

Comments
 (0)