From 826520c3ccc66cb682804b7fe1d5cda3c79b1d00 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Thu, 20 Nov 2025 12:40:01 +0000 Subject: [PATCH 1/3] Add token exchange guide for backend authentication Document how to configure RFC 8693 token exchange so MCP servers can authenticate to backend APIs using short-lived tokens instead of embedded secrets. Includes IdP configuration steps, CLI parameters, and an Okta example with annotated key points. Fixes: #240 --- docs/toolhive/guides-cli/token-exchange.mdx | 202 ++++++++++++++++++++ sidebars.ts | 1 + 2 files changed, 203 insertions(+) create mode 100644 docs/toolhive/guides-cli/token-exchange.mdx diff --git a/docs/toolhive/guides-cli/token-exchange.mdx b/docs/toolhive/guides-cli/token-exchange.mdx new file mode 100644 index 0000000..19aff46 --- /dev/null +++ b/docs/toolhive/guides-cli/token-exchange.mdx @@ -0,0 +1,202 @@ +--- +title: Configure token exchange for backend authentication +description: + How to set up token exchange so MCP servers can authenticate to backend + services using ToolHive. +--- + +This guide shows you how to configure token exchange, which allows MCP servers +to authenticate to backend APIs using short-lived, properly scoped tokens +instead of embedded secrets. + +For conceptual background on how token exchange works and its security benefits, +see [Backend authentication](../concepts/backend-auth.mdx), which includes a +[sequence diagram](../concepts/backend-auth.mdx#token-exchange-implementation) +illustrating the complete flow. + +## Prerequisites + +Before you begin, make sure you have: + +- ToolHive installed and working +- Basic familiarity with OAuth, OIDC, and JWT concepts +- An identity provider that supports + [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange (such + as Okta, Auth0, or Keycloak) +- A backend service configured to accept tokens from your identity provider +- Familiarity with [Authentication and authorization](./auth.mdx) setup in + ToolHive + +From your identity provider, you'll need: + +- Audience value for the MCP server +- Issuer URL +- JWKS URL (for key verification) +- Token exchange endpoint URL +- Client credentials for the token exchange client + +## Configure your identity provider + +Token exchange requires your identity provider to issue tokens for the backend +service when presented with a valid MCP server token. The exact configuration +steps vary by provider, but generally include: + +### Register a token exchange client + +Create an OAuth application in your identity provider for ToolHive to use when +performing token exchange: + +- Note the client ID and client secret +- Grant the application permission to use the + `urn:ietf:params:oauth:grant-type:token-exchange` grant type + +Token exchange is an authenticated flow—ToolHive uses these credentials to prove +its identity when requesting exchanged tokens from the identity provider. + +:::tip[Okta] + +Create an API Services application for ToolHive and enable the token exchange +grant type in the application settings. + +::: + +### Define audience and scopes for the backend service + +Configure your identity provider to recognize the backend service: + +- Define the audience value that identifies your backend service (for example, + `backend-api`) +- Specify the scopes the backend service accepts (for example, `api:read`, + `api:write`) + +:::tip[Okta] + +Create a custom authorization server for the backend service and define the +scopes under **Security > API > Authorization Servers**. + +::: + +### Create an access policy + +Set up a policy that permits token exchange and controls what scopes are +included in exchanged tokens: + +- Enable the token exchange grant type for the ToolHive client +- Define which users or groups can obtain tokens for the backend service +- Specify the scopes included in exchanged tokens + +:::tip[Okta] + +Add a trust relationship from the MCP authorization server to the backend +authorization server, then create access policies on the backend server to +permit token exchange. + +Consult the +[Okta token exchange documentation](https://developer.okta.com/docs/guides/set-up-token-exchange/main/) +for detailed steps. + +::: + +## MCP server requirements + +The MCP server that ToolHive fronts must accept a per-request authentication +token. Specifically, the server should: + +- Read the access token from the `Authorization: Bearer` header (or a custom + header if you configure `--token-exchange-header-name`) +- Use this token to authenticate to the backend service +- Not rely on hardcoded secrets or environment variables for backend + authentication + +ToolHive injects the exchanged token into each request, so the MCP server +receives a fresh, properly scoped token for every call. + +## Run an MCP server with token exchange + +Once your identity provider is configured, start your MCP server with token +exchange enabled: + +```bash +thv run \ + --oidc-audience \ + --oidc-issuer \ + --oidc-jwks-url \ + --token-exchange-url \ + --token-exchange-client-id \ + --token-exchange-client-secret \ + --token-exchange-audience \ + --token-exchange-scopes \ + +``` + +### Parameter reference + +| Parameter | Description | +| -------------------------------- | ------------------------------------------------------------------------- | +| `--oidc-*` | Standard OIDC parameters for validating incoming client tokens | +| `--token-exchange-url` | Your identity provider's token exchange endpoint | +| `--token-exchange-client-id` | Client ID for ToolHive to authenticate to the IdP | +| `--token-exchange-client-secret` | Client secret for ToolHive (or use `--token-exchange-client-secret-file`) | +| `--token-exchange-audience` | Target audience for the exchanged token (your backend service) | +| `--token-exchange-scopes` | Scopes to request for the backend service | + +Optional parameters: + +- `--token-exchange-header-name`: Custom header name for injecting the exchanged + token (defaults to replacing the `Authorization` header) +- `--token-exchange-subject-token-type`: Type of subject token to exchange; use + `id_token` for Google STS (defaults to `access_token`) + +## Verify the configuration + +To confirm token exchange is working: + +1. Connect to the MCP server with a client that supports authentication +2. Make a tool call that requires backend access +3. Check that the request succeeds and the backend receives a properly scoped + token + +You can also verify by examining your identity provider's logs for successful +token exchange requests, or by checking audit logs on your backend service to +confirm requests arrive with the correct user identity and scopes. + +## Example: Okta configuration + +This example shows a complete configuration for an MCP server that connects to a +backend API, using Okta for token exchange: + +```bash +thv run \ + --oidc-audience mcp-server \ + --oidc-issuer https://dev-123456.okta.com/oauth2/aus1234567890 \ + --oidc-jwks-url https://dev-123456.okta.com/oauth2/aus1234567890/v1/keys \ + --token-exchange-url https://dev-123456.okta.com/oauth2/aus9876543210/v1/token \ + --token-exchange-client-id 0oa0987654321fedcba \ + --token-exchange-client-secret-file /path/to/client-secret \ + --token-exchange-audience backend-api \ + --token-exchange-scopes "api:read,api:write" \ + +``` + +Key points in this example: + +- **Two authorization servers**: The `--oidc-issuer` URL (`aus1234567890`) is + the MCP authorization server that validates incoming client tokens. The + `--token-exchange-url` uses a different authorization server (`aus9876543210`) + that issues tokens for the backend API. +- **Audience transformation**: Client tokens arrive with audience `mcp-server`. + ToolHive exchanges them for tokens with audience `backend-api`, which the + backend service expects. +- **Scope transformation**: The original token has MCP-specific scopes (like + `mcp:tools:call`), while the exchanged token has backend-specific scopes + (`api:read`, `api:write`). The user's identity is preserved, but the + permissions are transformed for the target service. + +## Related information + +- [Backend authentication](../concepts/backend-auth.mdx) - conceptual overview + of token exchange and federation +- [Authentication and authorization](./auth.mdx) - basic auth setup for MCP + servers +- [CLI reference for `thv run`](../reference/cli/thv_run.md) - complete list of + flags diff --git a/sidebars.ts b/sidebars.ts index aa317a9..2adbabb 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -117,6 +117,7 @@ const sidebars: SidebarsConfig = { }, 'toolhive/guides-cli/telemetry-and-metrics', 'toolhive/guides-cli/auth', + 'toolhive/guides-cli/token-exchange', 'toolhive/guides-cli/test-mcp-servers', 'toolhive/guides-cli/build-containers', 'toolhive/guides-cli/advanced-cicd', From bb948fccfd9d7fe19de983ecc6543aa1b07b582f Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Thu, 20 Nov 2025 13:07:55 +0000 Subject: [PATCH 2/3] Add Kubernetes token exchange guide Document how to configure RFC 8693 token exchange for MCP servers in Kubernetes using the ToolHive Operator. Covers MCPExternalAuthConfig resource creation, Secret management, and MCPServer configuration with an Okta example. Fixes: #240 --- .../guides-k8s/token-exchange-k8s.mdx | 259 ++++++++++++++++++ sidebars.ts | 1 + 2 files changed, 260 insertions(+) create mode 100644 docs/toolhive/guides-k8s/token-exchange-k8s.mdx diff --git a/docs/toolhive/guides-k8s/token-exchange-k8s.mdx b/docs/toolhive/guides-k8s/token-exchange-k8s.mdx new file mode 100644 index 0000000..98ef6c7 --- /dev/null +++ b/docs/toolhive/guides-k8s/token-exchange-k8s.mdx @@ -0,0 +1,259 @@ +--- +title: Configure token exchange for backend authentication +description: + How to set up token exchange so MCP servers can authenticate to backend + services in Kubernetes using the ToolHive Operator. +--- + +This guide shows you how to configure token exchange in Kubernetes, which allows +MCP servers to authenticate to backend APIs using short-lived, properly scoped +tokens instead of embedded secrets. + +For conceptual background on how token exchange works, see +[Backend authentication](../concepts/backend-auth.mdx). For CLI-based setup, see +[Configure token exchange](../guides-cli/token-exchange.mdx). + +## Prerequisites + +Before you begin, make sure you have: + +- Kubernetes cluster with RBAC enabled +- ToolHive Operator installed (see + [Deploy the ToolHive Operator with Helm](./deploy-operator-helm.mdx)) +- `kubectl` access to your cluster +- An identity provider that supports + [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange (such + as Okta, Auth0, or Keycloak) +- A backend service configured to accept tokens from your identity provider +- Familiarity with + [Authentication and authorization in Kubernetes](./auth-k8s.mdx) + +## Configure your identity provider + +Token exchange requires your identity provider to issue tokens for the backend +service when presented with a valid MCP server token. This involves: + +- Registering a token exchange client with credentials +- Defining audience and scopes for the backend service +- Creating access policies that permit token exchange + +For detailed IdP configuration steps, see +[Configure your identity provider](../guides-cli/token-exchange.mdx#configure-your-identity-provider) +in the CLI guide. + +## Create the token exchange configuration + +### Step 1: Create a Secret for client credentials + +Store the OAuth client secret that ToolHive uses to authenticate when performing +token exchange: + +```yaml title="token-exchange-secret.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: token-exchange-secret + namespace: toolhive-system +type: Opaque +stringData: + client-secret: '' +``` + +```bash +kubectl apply -f token-exchange-secret.yaml +``` + +### Step 2: Create the MCPExternalAuthConfig resource + +Create an `MCPExternalAuthConfig` resource that defines the token exchange +parameters: + +```yaml title="token-exchange-config.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPExternalAuthConfig +metadata: + name: backend-token-exchange + namespace: toolhive-system +spec: + type: tokenExchange + tokenExchange: + tokenUrl: '' + audience: '' + clientId: '' + clientSecretRef: + name: token-exchange-secret + key: client-secret + scopes: + - '' +``` + +```bash +kubectl apply -f token-exchange-config.yaml +``` + +### Configuration reference + +| Field | Description | +| ----------------- | -------------------------------------------------------------- | +| `tokenUrl` | Your identity provider's token exchange endpoint | +| `audience` | Target audience for the exchanged token (your backend service) | +| `clientId` | Client ID for ToolHive to authenticate to the IdP | +| `clientSecretRef` | Reference to the Secret containing the client secret | +| `scopes` | Scopes to request for the backend service | + +## MCP server requirements + +The MCP server that ToolHive fronts must accept a per-request authentication +token. Specifically, the server should: + +- Read the access token from the `Authorization: Bearer` header +- Use this token to authenticate to the backend service +- Not rely on hardcoded secrets or environment variables for backend + authentication + +ToolHive injects the exchanged token into each request, so the MCP server +receives a fresh, properly scoped token for every call. + +## Deploy an MCP server with token exchange + +Create an `MCPServer` resource that references the token exchange configuration: + +```yaml title="mcpserver-token-exchange.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPServer +metadata: + name: my-mcp-server + namespace: toolhive-system +spec: + image: + transport: streamable-http + proxyPort: 8080 + # Reference the token exchange configuration + externalAuthConfigRef: + name: backend-token-exchange + # OIDC configuration for validating incoming client tokens + oidcConfig: + type: inline + inline: + issuer: '' + audience: '' + jwksUrl: '' +``` + +```bash +kubectl apply -f mcpserver-token-exchange.yaml +``` + +The `externalAuthConfigRef` tells ToolHive to use the token exchange +configuration you created earlier. The `oidcConfig` validates incoming client +tokens before performing the exchange. + +## Verify the configuration + +To confirm token exchange is working: + +1. Check the MCPServer status: + + ```bash + kubectl get mcpserver -n toolhive-system my-mcp-server + ``` + +2. Connect to the MCP server with a client that supports authentication + +3. Make a tool call that requires backend access + +4. Check the proxy logs for successful token exchange: + + ```bash + kubectl logs -n toolhive-system -l app.kubernetes.io/name=my-mcp-server + ``` + +You can also verify by examining your identity provider's logs for successful +token exchange requests, or by checking audit logs on your backend service to +confirm requests arrive with the correct user identity and scopes. + +## Example: Okta configuration + +This example shows a complete configuration using Okta for token exchange. + +### Secret + +```yaml title="okta-secret.yaml" +apiVersion: v1 +kind: Secret +metadata: + name: okta-token-exchange-secret + namespace: toolhive-system +type: Opaque +stringData: + client-secret: 'your-okta-client-secret' +``` + +### MCPExternalAuthConfig + +```yaml title="okta-token-exchange.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPExternalAuthConfig +metadata: + name: okta-backend-exchange + namespace: toolhive-system +spec: + type: tokenExchange + tokenExchange: + tokenUrl: 'https://dev-123456.okta.com/oauth2/aus9876543210/v1/token' + audience: 'backend-api' + clientId: '0oa0987654321fedcba' + clientSecretRef: + name: okta-token-exchange-secret + key: client-secret + scopes: + - 'api:read' + - 'api:write' +``` + +### MCPServer + +```yaml title="mcpserver-okta.yaml" +apiVersion: toolhive.stacklok.dev/v1alpha1 +kind: MCPServer +metadata: + name: my-backend-server + namespace: toolhive-system +spec: + image: your-mcp-server:latest + transport: streamable-http + proxyPort: 8080 + externalAuthConfigRef: + name: okta-backend-exchange + oidcConfig: + type: inline + inline: + issuer: 'https://dev-123456.okta.com/oauth2/aus1234567890' + audience: 'mcp-server' + jwksUrl: 'https://dev-123456.okta.com/oauth2/aus1234567890/v1/keys' +``` + +Key points in this example: + +- **Two authorization servers**: The `issuer` in `oidcConfig` (`aus1234567890`) + validates incoming client tokens. The `tokenUrl` in `MCPExternalAuthConfig` + uses a different authorization server (`aus9876543210`) that issues tokens for + the backend API. +- **Audience transformation**: Client tokens arrive with audience `mcp-server`. + ToolHive exchanges them for tokens with audience `backend-api`, which the + backend service expects. +- **Scope transformation**: The original token has MCP-specific scopes, while + the exchanged token has backend-specific scopes (`api:read`, `api:write`). The + user's identity is preserved, but the permissions are transformed for the + target service. + +## Related information + +- [Backend authentication](../concepts/backend-auth.mdx) - conceptual overview + of token exchange and federation +- [Configure token exchange (CLI)](../guides-cli/token-exchange.mdx) - CLI-based + setup +- [Authentication and authorization](./auth-k8s.mdx) - basic auth setup for MCP + servers in Kubernetes +- [CRD specification](../reference/crd-spec.mdx) - complete CRD reference + including MCPExternalAuthConfig diff --git a/sidebars.ts b/sidebars.ts index 2adbabb..b777527 100644 --- a/sidebars.ts +++ b/sidebars.ts @@ -158,6 +158,7 @@ const sidebars: SidebarsConfig = { 'toolhive/guides-k8s/telemetry-and-metrics', 'toolhive/guides-k8s/logging', 'toolhive/guides-k8s/auth-k8s', + 'toolhive/guides-k8s/token-exchange-k8s', 'toolhive/reference/crd-spec', ], }, From ac8f1af2f45e15e037bdd980edee1a671970e181 Mon Sep 17 00:00:00 2001 From: Jakub Hrozek Date: Fri, 21 Nov 2025 15:51:31 +0000 Subject: [PATCH 3/3] Use consistent placeholder style for server argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update placeholder from to to follow style guide convention (ALL_CAPS for placeholders) and clarify that users can pass either a registry name or direct image reference. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/toolhive/guides-cli/token-exchange.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/toolhive/guides-cli/token-exchange.mdx b/docs/toolhive/guides-cli/token-exchange.mdx index 19aff46..2c4c14a 100644 --- a/docs/toolhive/guides-cli/token-exchange.mdx +++ b/docs/toolhive/guides-cli/token-exchange.mdx @@ -126,7 +126,7 @@ thv run \ --token-exchange-client-secret \ --token-exchange-audience \ --token-exchange-scopes \ - + ``` ### Parameter reference @@ -175,7 +175,7 @@ thv run \ --token-exchange-client-secret-file /path/to/client-secret \ --token-exchange-audience backend-api \ --token-exchange-scopes "api:read,api:write" \ - + ``` Key points in this example: