diff --git a/docs/toolhive/concepts/embedded-auth-server.mdx b/docs/toolhive/concepts/embedded-auth-server.mdx index 0a0e9970..b6712dbc 100644 --- a/docs/toolhive/concepts/embedded-auth-server.mdx +++ b/docs/toolhive/concepts/embedded-auth-server.mdx @@ -149,6 +149,28 @@ authorization layers. ::: +### Minting delegated tokens with delegate clients + +The section above covers _reading_ an `act` claim that already exists on an +incoming token. ToolHive's embedded authorization server can also _mint_ that +claim itself, through RFC 8693 token-exchange delegation. + +Delegation exists because "an agent acting for a user" is a different trust +statement than either half of it alone. A token that only names the user can't +prove which agent is making the call; a token that only names the agent can't +prove the user consented to it acting on their behalf. RFC 8693 solves this by +letting a pre-provisioned **delegate client** (typically a coding agent or +automation workload) to exchange a user's subject token for a new +ToolHive-issued token that names the user as `sub` and records the delegate +client as the acting party in `act`, forming the same claim shape described +above. + +Delegation is configured with a `delegateClients` entry (the agent's +credentials) paired with a `trustedIssuers` entry that authorizes which external +actors and which delegate clients may perform the exchange. For the full +configuration walkthrough, see +[Delegate agent identity with token exchange](../guides-vmcp/delegate-agent-identity.mdx). + ### Delegation chains in audit logs Delegation is recorded on every call made with a delegated token, not once when diff --git a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx index 04c72a30..d6d8547e 100644 --- a/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx +++ b/docs/toolhive/guides-k8s/embedded-auth-server-k8s.mdx @@ -536,6 +536,12 @@ 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. +To accept subject tokens minted by an external OIDC issuer, pair this with a +`trustedIssuers` entry. For why delegation exists, the consent policy options, +and a worked exchange, see +[Delegate agent identity with token exchange](../guides-vmcp/delegate-agent-identity.mdx). +The same fields are available here through `MCPExternalAuthConfig`. + ### 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/accept-workload-assertions.mdx b/docs/toolhive/guides-vmcp/accept-workload-assertions.mdx new file mode 100644 index 00000000..675847ed --- /dev/null +++ b/docs/toolhive/guides-vmcp/accept-workload-assertions.mdx @@ -0,0 +1,175 @@ +--- +title: Accept workload assertions with the JWT-bearer grant +description: + Configure the RFC 7523 JWT-bearer grant so a trusted workload can get a + ToolHive token by presenting a signed assertion, with no client registration + at all. +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +An AI agent isn't a person who can type a password, and it usually isn't a +long-lived service account either. It needs to prove who it is every time it +runs, using whatever identity its platform already issued it (a cloud IAM role, +a Kubernetes-native workload identity, or a service identity from your IdP). The +JWT-bearer grant lets an agent use that identity directly to call a tool through +vMCP: it presents a signed assertion from its identity provider straight to +ToolHive's token endpoint, and ToolHive exchanges it for a ToolHive token, with +no ToolHive client registration, no shared secret, nothing for an admin to +provision ahead of time beyond trusting the issuer itself. + +:::info + +The `trustedIssuers[].jwtBearerGrant` field covered here is also available on a +plain `MCPServer` through `MCPExternalAuthConfig`'s `embeddedAuthServer` block, +using the same shape shown below under `authServerConfig`. For the MCPServer +field reference, see +[Set up the embedded authorization server in Kubernetes](../guides-k8s/embedded-auth-server-k8s.mdx). + +::: + +If you're choosing between mechanisms, see the comparison table in +[Delegate agent identity with token exchange](./delegate-agent-identity.mdx#overview). +The JWT-bearer grant answers "how does a workload with no registered client get +a token at all?". RFC 8693 delegation answers a different question: "who is this +agent acting for?" + +## How the grant works + +The workload sends its assertion straight to `/oauth/token`; possession of the +assertion is the only credential ToolHive checks: + +```bash title="POST /oauth/token" +curl -s -X POST https://vmcp.example.com/oauth/token \ + -d "grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer" \ + -d "assertion=" \ + -d "resource=https://vmcp.example.com/mcp-resource" +``` + +ToolHive mints a token for a synthetic client derived deterministically from the +assertion's issuer and subject. There's no delegation and nothing to +pre-register. To accept the assertion in the first place, ToolHive needs a +`trustedIssuers[].jwtBearerGrant` entry naming the workload's subject and the +resource it may request. What that entry looks like depends on the identity +provider: + + + + +Okta's Custom Authorization Server lets you set its `audiences` field to an +arbitrary caller-chosen string, so you can register it as the exact ToolHive +token endpoint the workload's assertion will be presented to: + +```yaml title="VirtualMCPServer: jwtBearerGrant policy" +spec: + authServerConfig: + issuer: https://vmcp.example.com + trustedIssuers: + - issuerUrl: 'https://.okta.com/oauth2/' + jwksUrl: 'https://.okta.com/oauth2//v1/keys' + jwtBearerGrant: + maxAssertionAge: 5m + subjectBindings: + - subject: '' + allowedResources: + - https://vmcp.example.com/mcp-resource +``` + + + + +An Entra Application ID URI is fixed when you register it and can't reference a +per-deployment in-cluster hostname that doesn't exist yet. Register a stable +Identifier URI on the Entra app ahead of time and widen `acceptedAudiences` to +accept it instead of the real token endpoint: + +```yaml title="VirtualMCPServer: jwtBearerGrant with a fixed external audience" +trustedIssuers: + - issuerUrl: 'https://sts.windows.net//' + jwksUrl: 'https://login.windows.net/common/discovery/keys' + jwtBearerGrant: + maxAssertionAge: 70m + # Entra's Identifier URI is fixed at registration time and can't equal + # this deployment's real (dynamic) token endpoint, so accept it instead. + acceptedAudiences: + - 'https://.onmicrosoft.com/toolhive-as-jwtbearer/oauth/token' + subjectBindings: + - subject: '' + allowedResources: + - https://vmcp.example.com/mcp-resource +``` + + + + +A SPIRE-attested workload needs no shared secret anywhere in the flow. Its +identity comes entirely from attestation (for example, "this pod, in this +namespace, with this service account, in this trust domain"), and it requests +ToolHive's real token endpoint as its JWT-SVID's audience directly, so no +`acceptedAudiences` override is needed. The one thing that does need a +workaround: SPIRE's OIDC discovery provider only serves its JWKS over HTTPS with +a SPIRE-internal CA-issued certificate, and `jwksUrl` has no CA-bundle option. +Mirror the same keys over plain HTTP instead (fine inside the cluster network; +not how you'd expose this across a real trust boundary): + +```yaml title="VirtualMCPServer: jwtBearerGrant for a SPIRE trust domain" +trustedIssuers: + - issuerUrl: 'https://oidc-discovery.' + jwksUrl: 'http://spire-jwks-mirror..svc.cluster.local:8000/keys.json' + insecureAllowHTTP: true + allowPrivateIPs: true + jwtBearerGrant: + maxAssertionAge: 70m + subjectBindings: + - subject: 'spiffe:///ns//sa/' + allowedResources: + - https://vmcp.example.com/mcp-resource +``` + + + + +`expectedAudience`, `actorClaim`, `actorMatcher`, and `allowMayAct` on a +`trustedIssuers` entry are all delegation-specific and unrelated to +`jwtBearerGrant`; an issuer used only for the JWT-bearer grant needs none of +them. + +## Troubleshooting + +Two things vary by issuer and are worth checking first if an exchange fails: +whether the assertion carries a `jti` at all (Entra's `client_credentials` +tokens and plain SPIRE JWT-SVIDs never include one, so ToolHive falls back to +hashing the raw assertion for replay protection instead), and whether the +assertion's `aud` needs an `acceptedAudiences` entry to match, per the tabs +above. + +| Error | Likely cause | +| ------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | +| `invalid_grant`: "The JWT bearer assertion issuer is not enabled for this grant." | The assertion's `iss` doesn't match a `trustedIssuers` entry with `jwtBearerGrant` configured. | +| `invalid_grant`: "The JWT bearer assertion subject is not configured for this grant." | The assertion's `sub` has no matching entry in `jwtBearerGrant.subjectBindings`. | +| `invalid_target` | The `resource` parameter isn't in the matched subject binding's `allowedResources`. | +| `invalid_grant`: "The JWT bearer assertion has already been used." | The assertion's replay key (its `jti`, or a hash of the assertion when `jti` is absent) was already consumed. | + +## Next steps + +- [Delegate agent identity with token exchange](./delegate-agent-identity.mdx) + to record which user an agent is acting for +- [Configure the vMCP embedded authorization server](./embedded-auth-server-vmcp.mdx) + for the rest of the `authServerConfig` surface + +## Related information + +- [Secretless delegate clients with `private_key_jwt`](./delegate-agent-identity.mdx#secretless-delegate-clients-with-private_key_jwt) - + a different mechanism that's easy to confuse with this one, since both come + from RFC 7523. That one is how an already-registered client proves its + identity instead of using a secret; the JWT-bearer grant covered on this page + has no client at all. +- [Delegate agent identity with token exchange](./delegate-agent-identity.mdx) + for RFC 8693 delegation and the full comparison of authentication patterns +- [Embedded authorization server](../concepts/embedded-auth-server.mdx) for the + OAuth flow, token storage, and the `act` claim +- [Configure the vMCP embedded authorization server](./embedded-auth-server-vmcp.mdx) + for the full `authServerConfig` reference +- [MCPExternalAuthConfig reference](../reference/crds/mcpexternalauthconfig.mdx) + for the complete field list diff --git a/docs/toolhive/guides-vmcp/delegate-agent-identity.mdx b/docs/toolhive/guides-vmcp/delegate-agent-identity.mdx new file mode 100644 index 00000000..a22bae68 --- /dev/null +++ b/docs/toolhive/guides-vmcp/delegate-agent-identity.mdx @@ -0,0 +1,298 @@ +--- +title: Delegate agent identity with token exchange +description: + Configure RFC 8693 token-exchange delegation so a pre-provisioned agent can + act on behalf of a user, including consent policy and secretless + private_key_jwt registration. +--- + +When an AI agent acts on a user's behalf (reading their calendar, filing a +ticket, calling a tool through vMCP), you usually want ToolHive to record both +facts: who the human is, and which agent actually made the call, so audit logs +and authorization policies can tell "Alice, directly" apart from "an agent, +acting for Alice." RFC 8693 token-exchange delegation is how ToolHive does this: +a pre-provisioned client exchanges a user's token for one that names both the +user and the agent acting for them. + +:::info + +The fields covered here (`trustedIssuers` and `delegateClients`) are also +available on a plain `MCPServer` through `MCPExternalAuthConfig`'s +`embeddedAuthServer` block, using the same shape shown below under +`authServerConfig`. This page focuses on VirtualMCPServer because delegation is +primarily useful when an agent orchestrates calls across multiple backends on a +user's behalf. For the MCPServer field reference, see +[Set up the embedded authorization server in Kubernetes](../guides-k8s/embedded-auth-server-k8s.mdx). + +::: + +## Overview + +The embedded authorization server supports four ways a caller can get a +ToolHive-issued token, each suited to a different trust relationship: + +| Pattern | Who authenticates | Result | +| ---------------------------------------------------------------------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------- | +| [Confidential DCR](./embedded-auth-server-vmcp.mdx#allow-confidential-dcr-clients) | A client self-registers and gets a `client_secret` | The client acts as itself; no delegation | +| [Backend token exchange](../concepts/backend-auth.mdx#token-exchange) | A client presents its own token to a backend IdP | The client's own identity, re-scoped to a backend audience | +| RFC 8693 delegate-client exchange (this page) | A pre-provisioned client presents a user's subject token | A token naming the user, with the agent recorded as the acting party (`act`) | +| [RFC 7523 JWT-bearer grant](./accept-workload-assertions.mdx) | No client at all; the assertion itself is the credential | A token naming the workload; no delegation, no pre-registered client | + +Delegation answers "who is this agent acting for?" A related but separate +mechanism, the RFC 7523 JWT-bearer grant, answers "how does a workload with no +registered client get a token at all?" See +[Accept workload assertions with the JWT-bearer grant](./accept-workload-assertions.mdx) +for that mechanism. + +This page covers how ToolHive **mints** a delegated token. If you're looking for +how ToolHive **reads** an `act` claim on an incoming token, for example because +your own IdP already performs RFC 8693 delegation upstream of ToolHive, see +[Delegated identities and the `act` claim](../concepts/embedded-auth-server.mdx#delegated-identities-and-the-act-claim). + +## RFC 8693 delegation with a pre-provisioned delegate client + +Delegation requires two pieces of configuration on `authServerConfig`: a +`delegateClients` entry for the agent that will perform the exchange, and a +`trustedIssuers` entry that tells ToolHive which external issuer's tokens it +will accept as a subject token, and which actors are allowed to act on behalf of +the tokens it issues. + +For how to create the `delegateClients` entry itself (the client ID, secret, +audiences, and scopes), see +[Pre-provision confidential clients for token exchange](./embedded-auth-server-vmcp.mdx#pre-provision-confidential-clients-for-token-exchange). +`delegateClients` is orthogonal to Dynamic Client Registration (plain, +confidential, or +[`private_key_jwt`](#secretless-delegate-clients-with-private_key_jwt)). A +`delegateClients` entry has a `clientId` and secret you choose and create +yourself, never one a client obtains by registering itself. A client uses one +path or the other; `trustedIssuers[].allowedDelegateClients` (below) is what +authorizes either kind of `client_id` to actually perform an exchange, once it +has one. + +Both entries are required. `trustedIssuers` grants a delegate client permission +to accept a subject token from the external issuer; on its own it doesn't create +a usable exchange path, because RFC 8693 requires authenticated access to +`/oauth/token` and the delegate client is what authenticates. + +This section focuses on the `trustedIssuers` side, which is what actually +authorizes delegation: + +```yaml title="VirtualMCPServer: delegation configuration" +spec: + authServerConfig: + issuer: https://vmcp.example.com + # ... + delegateClients: + - clientId: coding-agent + clientSecretRef: + name: coding-agent-secret + key: client-secret + scopes: + - openid + audiences: + - https://vmcp.example.com/mcp-resource + trustedIssuers: + - issuerUrl: 'https://sts.windows.net//' + expectedAudience: 'https://vmcp.example.com/mcp-resource' + jwksUrl: 'https://login.windows.net//discovery/v2.0/keys' + # "appid" is where Microsoft Entra v1 tokens carry the calling + # application's client ID, verified against a real Entra tenant. + # Other issuers use a different claim name for the same purpose (for + # example Okta's client_credentials tokens use "cid"); check your + # issuer's own token shape, or use the literal value "client_id" + # (a sentinel, not a claim name) to read the token's client_id claim. + actorClaim: appid + allowedActors: + - + allowedDelegateClients: + - coding-agent +``` + +`trustedIssuers[].issuerUrl` and `jwksUrl` identify the external identity +provider that minted the subject token the agent will present (in this example, +a user's Entra sign-in as a separate application, `App1`). `expectedAudience` +must match the audience already present on that subject token. `actorClaim` +names the claim carrying the calling application's client ID; it defaults to +`azp` when you leave it unset. `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. Set it to `["*"]` to declare that permissiveness +explicitly; the wildcard must stand alone, and combining it with specific client +IDs is rejected at admission. + +For the complete `trustedIssuers` field list, including `jwksUrl`, +`allowPrivateIPs`, and the per-issuer `insecureAllowHTTP`, see the +[MCPExternalAuthConfig schema reference](../reference/crds/mcpexternalauthconfig.mdx). + +### Walk through an exchange + +Once configured, the agent (`coding-agent`) presents a user's subject token to +ToolHive's `/oauth/token` endpoint using its own client credentials: + +```bash +curl -s -X POST https://vmcp.example.com/oauth/token \ + -u "coding-agent:" \ + -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + -d "subject_token=" \ + -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \ + -d "audience=https://vmcp.example.com/mcp-resource" +``` + +`SUBJECT_TOKEN` is the user's own token from the external issuer. In this +example, an Entra access token issued to `App1` with `aud` set to the backend +resource and `appid` set to `App1`'s client ID. ToolHive validates that token +against the matching `trustedIssuers` entry, confirms `App1` is an allowed +actor, and confirms `coding-agent` is an allowed delegate client, then issues a +delegated access token. Decoding it shows the delegation: + +```json +{ + "sub": "https://sts.windows.net//#", + "act": { + "iss": "https://vmcp.example.com", + "sub": "coding-agent", + "act": { + "iss": "https://sts.windows.net//", + "sub": "" + } + } +} +``` + +`sub` identifies the user (qualified with the issuer to avoid collisions across +identity providers). The outer `act` names the authenticated ToolHive client +that performed the exchange, in `act.sub`, with `act.iss` set to the ToolHive +issuer that minted the token. The nested `act.act` is the external actor +asserted by the subject token itself, the application the user actually signed +in through. This nested shape is how ToolHive represents a two-hop delegation +chain: an external application acting for a user, and a ToolHive client acting +on top of that. + +## Choosing a consent policy + +`trustedIssuers` entries support three independent ways to authorize delegation +from an external issuer. Any one of them being satisfied is sufficient; they +aren't layered as an all-must-pass chain: + +| Field | How it authorizes | +| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `allowedActors` | A static allowlist of `actorClaim` values. Use this for a fixed, known set of external applications (like `App1` above). | +| `actorMatcher` | An admin-authored CEL expression evaluated against the subject token's complete, signature-verified claims map (bound as `claims`). Use this when the authorization rule can't be expressed as a flat allowlist, for example matching on a claim pattern or a combination of claims. Must evaluate to a boolean; a non-boolean result denies the token at evaluation time. | +| `allowMayAct` | Trusts a `may_act` claim the external issuer itself already asserts on the subject token, bypassing `allowedActors` and `actorMatcher` entirely. Defaults to `false`; external issuers must be opted in explicitly, since `may_act` shifts the consent decision to the external IdP. Doesn't apply to self-issued subject tokens. Enabling it alongside `allowedDelegateClients: ["*"]` is rejected at admission. | + +`allowedDelegateClients` is a separate, always-required control: it restricts +_which ToolHive clients_ may perform the exchange, independent of which external +actor the subject token names. `allowedActors`, `actorMatcher`, and +`allowMayAct` all authorize the external actor; `allowedDelegateClients` +authorizes the ToolHive-side client. + +## Secretless delegate clients with `private_key_jwt` + +:::info + +`private_key_jwt` client registration and authentication is not yet part of a +released ToolHive version. + +::: + +A `delegateClients` entry requires ToolHive to hold a shared secret for the +agent. Setting `allowPrivateKeyJWTRegistration: true` on `authServerConfig` +instead lets an agent register itself via Dynamic Client Registration (DCR) +using only a keypair it generates locally. ToolHive never issues, stores, or +transmits a secret for that client. + +```yaml title="VirtualMCPServer: allow private_key_jwt registration" +spec: + authServerConfig: + issuer: https://vmcp.example.com + # highlight-next-line + allowPrivateKeyJWTRegistration: true +``` + +Registration is unauthenticated, the same as ordinary DCR, so enabling this lets +any caller who can reach `/oauth/register` register a `private_key_jwt` client. +It cannot be combined with `insecureAllowHTTP: true`, since `private_key_jwt` +registration would then occur over cleartext HTTP. + +If you're comparing ways to avoid provisioning a client secret at all, see also +[Client ID Metadata Document (CIMD)](../concepts/embedded-auth-server.mdx#client-id-metadata-document-cimd), +which lets a client authenticate from a hosted metadata document instead of +either a shared secret or a keypair registered through DCR. CIMD doesn't involve +delegation, so it's unrelated to the exchange described on this page. + +### Register and authenticate with a keypair + +The agent generates an RSA (or EC) keypair and declares only the public half at +registration: + +```bash title="POST /oauth/register" +curl -s -X POST https://vmcp.example.com/oauth/register \ + -H "Content-Type: application/json" \ + -d '{ + "redirect_uris": ["http://localhost:19999/callback"], + "token_endpoint_auth_method": "private_key_jwt", + "token_endpoint_auth_signing_alg": "RS256", + "grant_types": ["urn:ietf:params:oauth:grant-type:token-exchange"], + "jwks": {"keys": [{"kty": "RSA", "use": "sig", "alg": "RS256", "kid": "agent-key", "n": "", "e": "AQAB"}]} + }' +``` + +The response carries a `client_id` and **no `client_secret`**. The keypair +itself is the credential. To authenticate at the token endpoint, the agent signs +a `client_assertion` JWT with its private key (`iss` and `sub` set to its own +`client_id`, `aud` set to ToolHive's token endpoint, with a unique `jti`), then +presents it alongside the subject token: + +```bash title="POST /oauth/token" +curl -s -X POST https://vmcp.example.com/oauth/token \ + -d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \ + -d "subject_token=" \ + -d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \ + -d "audience=https://vmcp.example.com/mcp-resource" \ + -d "client_id=" \ + -d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \ + -d "client_assertion=" +``` + +No `client_secret` parameter appears anywhere in this request. ToolHive +validates the `client_assertion`'s signature against the JWKS declared at +registration and rejects a replayed `client_assertion` (the same `jti` presented +twice) on the second attempt. Everything else about the exchange. Subject-token +validation against `trustedIssuers`, actor resolution, and the resulting `act` +claim, works exactly as in the pre-provisioned case above; the issued token's +`act.sub` is the DCR-assigned `client_id` instead of a statically configured +one. + +## Troubleshooting + +| Error | Likely cause | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `invalid_client` (exact hint varies by cause: wrong secret, wrong `token_endpoint_auth_method`, or a public client attempting a confidential-only grant) | The delegate client must be confidential. Verify `delegateClients[].clientSecretRef` is set and the client authenticated with the matching secret and method. | +| `invalid_grant`: "The subject token does not authorize this client to act on behalf of the subject." | The subject token's `may_act.sub` doesn't match the actor ToolHive resolved for this request. | +| `invalid_grant`: "This client is not authorized to exchange subject tokens from the external actor's issuer." | The authenticated client isn't in `trustedIssuers[].allowedDelegateClients` for the issuer that minted the subject token. | +| `invalid_grant`: "The subject token was issued to a different client." | The subject token's own `client_id` claim doesn't match the authenticated delegate client, and the client isn't in `allowedDelegateClients`. | +| `invalid_request`: "The subject token is invalid or could not be verified." | The subject token failed signature, issuer, or audience validation against every configured `trustedIssuers` entry. | +| CRD rejected: "allowedDelegateClients is required when expectedAudience, actorClaim, actorMatcher, or allowMayAct is set" | A `trustedIssuers` entry configures delegation fields without also setting `allowedDelegateClients`. Add it, or remove the delegation fields if only `jwtBearerGrant` is needed. | +| CRD rejected: "allowConfidentialClientRegistration cannot be combined with insecureAllowHTTP; client secrets would be issued in cleartext over an unauthenticated endpoint" (or the `allowPrivateKeyJWTRegistration` equivalent) | `authServerConfig.issuer` uses `http://` while confidential DCR or `private_key_jwt` registration is enabled. The CEL admission rule can't express the loopback exception, so use an `https://` issuer. For a plain-HTTP loopback issuer in local development, set `insecureAllowConfidentialOverLoopbackHTTP`. | + +## Next steps + +- [Accept workload assertions with the JWT-bearer grant](./accept-workload-assertions.mdx) + to let a workload authenticate with no registered client at all +- [Read the `act` claim in Cedar policies](../concepts/embedded-auth-server.mdx#reading-the-act-claim-in-cedar-policies) + to authorize on the delegation chain you just configured +- [Configure the vMCP embedded authorization server](./embedded-auth-server-vmcp.mdx) + for the rest of the `authServerConfig` surface + +## Related information + +- [Embedded authorization server](../concepts/embedded-auth-server.mdx) for the + OAuth flow, token storage, and the `act` claim on the reading side +- [Configure the vMCP embedded authorization server](./embedded-auth-server-vmcp.mdx) + for the full `authServerConfig` reference, including `delegateClients` setup +- [Accept workload assertions with the JWT-bearer grant](./accept-workload-assertions.mdx) + for clientless workload authentication under RFC 7523 ยง2.1 +- [Backend authentication](../concepts/backend-auth.mdx) for the other backend + authentication patterns +- [MCPExternalAuthConfig reference](../reference/crds/mcpexternalauthconfig.mdx) + for the complete field list diff --git a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx index 1a27d490..34defe55 100644 --- a/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx +++ b/docs/toolhive/guides-vmcp/embedded-auth-server-vmcp.mdx @@ -354,6 +354,11 @@ 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. +To accept subject tokens minted by an external OIDC issuer, pair this with a +`trustedIssuers` entry. For why delegation exists, the consent policy options, +and a worked exchange, see +[Delegate agent identity with token exchange](./delegate-agent-identity.mdx). + ### Enable baseline scopes for DCR clients If your MCP clients register via DCR with a narrowed `scope` value and then diff --git a/sidebars.ts b/sidebars.ts index e37b8cb7..3f72c20d 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -235,7 +235,11 @@ const mcpSidebar: SidebarsConfig[string] = [ type: 'doc', id: 'toolhive/guides-vmcp/authentication', }, - items: ['toolhive/guides-vmcp/embedded-auth-server-vmcp'], + items: [ + 'toolhive/guides-vmcp/embedded-auth-server-vmcp', + 'toolhive/guides-vmcp/delegate-agent-identity', + 'toolhive/guides-vmcp/accept-workload-assertions', + ], }, 'toolhive/guides-vmcp/tool-aggregation', 'toolhive/guides-vmcp/composite-tools',