|
| 1 | +--- |
| 2 | +title: Configure token exchange for backend authentication |
| 3 | +description: |
| 4 | + How to set up token exchange so MCP servers can authenticate to backend |
| 5 | + services in Kubernetes using the ToolHive Operator. |
| 6 | +--- |
| 7 | + |
| 8 | +This guide shows you how to configure token exchange in Kubernetes, which allows |
| 9 | +MCP servers to authenticate to backend APIs using short-lived, properly scoped |
| 10 | +tokens instead of embedded secrets. |
| 11 | + |
| 12 | +For conceptual background on how token exchange works, see |
| 13 | +[Backend authentication](../concepts/backend-auth.mdx). For CLI-based setup, see |
| 14 | +[Configure token exchange](../guides-cli/token-exchange.mdx). |
| 15 | + |
| 16 | +## Prerequisites |
| 17 | + |
| 18 | +Before you begin, make sure you have: |
| 19 | + |
| 20 | +- Kubernetes cluster with RBAC enabled |
| 21 | +- ToolHive Operator installed (see |
| 22 | + [Deploy the ToolHive Operator with Helm](./deploy-operator-helm.mdx)) |
| 23 | +- `kubectl` access to your cluster |
| 24 | +- An identity provider that supports |
| 25 | + [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange (such |
| 26 | + as Okta, Auth0, or Keycloak) |
| 27 | +- A backend service configured to accept tokens from your identity provider |
| 28 | +- Familiarity with |
| 29 | + [Authentication and authorization in Kubernetes](./auth-k8s.mdx) |
| 30 | + |
| 31 | +## Configure your identity provider |
| 32 | + |
| 33 | +Token exchange requires your identity provider to issue tokens for the backend |
| 34 | +service when presented with a valid MCP server token. This involves: |
| 35 | + |
| 36 | +- Registering a token exchange client with credentials |
| 37 | +- Defining audience and scopes for the backend service |
| 38 | +- Creating access policies that permit token exchange |
| 39 | + |
| 40 | +For detailed IdP configuration steps, see |
| 41 | +[Configure your identity provider](../guides-cli/token-exchange.mdx#configure-your-identity-provider) |
| 42 | +in the CLI guide. |
| 43 | + |
| 44 | +## Create the token exchange configuration |
| 45 | + |
| 46 | +### Step 1: Create a Secret for client credentials |
| 47 | + |
| 48 | +Store the OAuth client secret that ToolHive uses to authenticate when performing |
| 49 | +token exchange: |
| 50 | + |
| 51 | +```yaml title="token-exchange-secret.yaml" |
| 52 | +apiVersion: v1 |
| 53 | +kind: Secret |
| 54 | +metadata: |
| 55 | + name: token-exchange-secret |
| 56 | + namespace: toolhive-system |
| 57 | +type: Opaque |
| 58 | +stringData: |
| 59 | + client-secret: '<YOUR_CLIENT_SECRET>' |
| 60 | +``` |
| 61 | +
|
| 62 | +```bash |
| 63 | +kubectl apply -f token-exchange-secret.yaml |
| 64 | +``` |
| 65 | + |
| 66 | +### Step 2: Create the MCPExternalAuthConfig resource |
| 67 | + |
| 68 | +Create an `MCPExternalAuthConfig` resource that defines the token exchange |
| 69 | +parameters: |
| 70 | + |
| 71 | +```yaml title="token-exchange-config.yaml" |
| 72 | +apiVersion: toolhive.stacklok.dev/v1alpha1 |
| 73 | +kind: MCPExternalAuthConfig |
| 74 | +metadata: |
| 75 | + name: backend-token-exchange |
| 76 | + namespace: toolhive-system |
| 77 | +spec: |
| 78 | + type: tokenExchange |
| 79 | + tokenExchange: |
| 80 | + tokenUrl: '<YOUR_TOKEN_EXCHANGE_URL>' |
| 81 | + audience: '<YOUR_BACKEND_AUDIENCE>' |
| 82 | + clientId: '<YOUR_CLIENT_ID>' |
| 83 | + clientSecretRef: |
| 84 | + name: token-exchange-secret |
| 85 | + key: client-secret |
| 86 | + scopes: |
| 87 | + - '<YOUR_REQUIRED_SCOPES>' |
| 88 | +``` |
| 89 | +
|
| 90 | +```bash |
| 91 | +kubectl apply -f token-exchange-config.yaml |
| 92 | +``` |
| 93 | + |
| 94 | +### Configuration reference |
| 95 | + |
| 96 | +| Field | Description | |
| 97 | +| ----------------- | -------------------------------------------------------------- | |
| 98 | +| `tokenUrl` | Your identity provider's token exchange endpoint | |
| 99 | +| `audience` | Target audience for the exchanged token (your backend service) | |
| 100 | +| `clientId` | Client ID for ToolHive to authenticate to the IdP | |
| 101 | +| `clientSecretRef` | Reference to the Secret containing the client secret | |
| 102 | +| `scopes` | Scopes to request for the backend service | |
| 103 | + |
| 104 | +## MCP server requirements |
| 105 | + |
| 106 | +The MCP server that ToolHive fronts must accept a per-request authentication |
| 107 | +token. Specifically, the server should: |
| 108 | + |
| 109 | +- Read the access token from the `Authorization: Bearer` header |
| 110 | +- Use this token to authenticate to the backend service |
| 111 | +- Not rely on hardcoded secrets or environment variables for backend |
| 112 | + authentication |
| 113 | + |
| 114 | +ToolHive injects the exchanged token into each request, so the MCP server |
| 115 | +receives a fresh, properly scoped token for every call. |
| 116 | + |
| 117 | +## Deploy an MCP server with token exchange |
| 118 | + |
| 119 | +Create an `MCPServer` resource that references the token exchange configuration: |
| 120 | + |
| 121 | +```yaml title="mcpserver-token-exchange.yaml" |
| 122 | +apiVersion: toolhive.stacklok.dev/v1alpha1 |
| 123 | +kind: MCPServer |
| 124 | +metadata: |
| 125 | + name: my-mcp-server |
| 126 | + namespace: toolhive-system |
| 127 | +spec: |
| 128 | + image: <YOUR_MCP_SERVER_IMAGE> |
| 129 | + transport: streamable-http |
| 130 | + proxyPort: 8080 |
| 131 | + # Reference the token exchange configuration |
| 132 | + externalAuthConfigRef: |
| 133 | + name: backend-token-exchange |
| 134 | + # OIDC configuration for validating incoming client tokens |
| 135 | + oidcConfig: |
| 136 | + type: inline |
| 137 | + inline: |
| 138 | + issuer: '<YOUR_OIDC_ISSUER>' |
| 139 | + audience: '<YOUR_MCP_AUDIENCE>' |
| 140 | + jwksUrl: '<YOUR_JWKS_URL>' |
| 141 | +``` |
| 142 | +
|
| 143 | +```bash |
| 144 | +kubectl apply -f mcpserver-token-exchange.yaml |
| 145 | +``` |
| 146 | + |
| 147 | +The `externalAuthConfigRef` tells ToolHive to use the token exchange |
| 148 | +configuration you created earlier. The `oidcConfig` validates incoming client |
| 149 | +tokens before performing the exchange. |
| 150 | + |
| 151 | +## Verify the configuration |
| 152 | + |
| 153 | +To confirm token exchange is working: |
| 154 | + |
| 155 | +1. Check the MCPServer status: |
| 156 | + |
| 157 | + ```bash |
| 158 | + kubectl get mcpserver -n toolhive-system my-mcp-server |
| 159 | + ``` |
| 160 | + |
| 161 | +2. Connect to the MCP server with a client that supports authentication |
| 162 | + |
| 163 | +3. Make a tool call that requires backend access |
| 164 | + |
| 165 | +4. Check the proxy logs for successful token exchange: |
| 166 | + |
| 167 | + ```bash |
| 168 | + kubectl logs -n toolhive-system -l app.kubernetes.io/name=my-mcp-server |
| 169 | + ``` |
| 170 | + |
| 171 | +You can also verify by examining your identity provider's logs for successful |
| 172 | +token exchange requests, or by checking audit logs on your backend service to |
| 173 | +confirm requests arrive with the correct user identity and scopes. |
| 174 | + |
| 175 | +## Example: Okta configuration |
| 176 | + |
| 177 | +This example shows a complete configuration using Okta for token exchange. |
| 178 | + |
| 179 | +### Secret |
| 180 | + |
| 181 | +```yaml title="okta-secret.yaml" |
| 182 | +apiVersion: v1 |
| 183 | +kind: Secret |
| 184 | +metadata: |
| 185 | + name: okta-token-exchange-secret |
| 186 | + namespace: toolhive-system |
| 187 | +type: Opaque |
| 188 | +stringData: |
| 189 | + client-secret: 'your-okta-client-secret' |
| 190 | +``` |
| 191 | +
|
| 192 | +### MCPExternalAuthConfig |
| 193 | +
|
| 194 | +```yaml title="okta-token-exchange.yaml" |
| 195 | +apiVersion: toolhive.stacklok.dev/v1alpha1 |
| 196 | +kind: MCPExternalAuthConfig |
| 197 | +metadata: |
| 198 | + name: okta-backend-exchange |
| 199 | + namespace: toolhive-system |
| 200 | +spec: |
| 201 | + type: tokenExchange |
| 202 | + tokenExchange: |
| 203 | + tokenUrl: 'https://dev-123456.okta.com/oauth2/aus9876543210/v1/token' |
| 204 | + audience: 'backend-api' |
| 205 | + clientId: '0oa0987654321fedcba' |
| 206 | + clientSecretRef: |
| 207 | + name: okta-token-exchange-secret |
| 208 | + key: client-secret |
| 209 | + scopes: |
| 210 | + - 'api:read' |
| 211 | + - 'api:write' |
| 212 | +``` |
| 213 | +
|
| 214 | +### MCPServer |
| 215 | +
|
| 216 | +```yaml title="mcpserver-okta.yaml" |
| 217 | +apiVersion: toolhive.stacklok.dev/v1alpha1 |
| 218 | +kind: MCPServer |
| 219 | +metadata: |
| 220 | + name: my-backend-server |
| 221 | + namespace: toolhive-system |
| 222 | +spec: |
| 223 | + image: your-mcp-server:latest |
| 224 | + transport: streamable-http |
| 225 | + proxyPort: 8080 |
| 226 | + externalAuthConfigRef: |
| 227 | + name: okta-backend-exchange |
| 228 | + oidcConfig: |
| 229 | + type: inline |
| 230 | + inline: |
| 231 | + issuer: 'https://dev-123456.okta.com/oauth2/aus1234567890' |
| 232 | + audience: 'mcp-server' |
| 233 | + jwksUrl: 'https://dev-123456.okta.com/oauth2/aus1234567890/v1/keys' |
| 234 | +``` |
| 235 | +
|
| 236 | +Key points in this example: |
| 237 | +
|
| 238 | +- **Two authorization servers**: The `issuer` in `oidcConfig` (`aus1234567890`) |
| 239 | + validates incoming client tokens. The `tokenUrl` in `MCPExternalAuthConfig` |
| 240 | + uses a different authorization server (`aus9876543210`) that issues tokens for |
| 241 | + the backend API. |
| 242 | +- **Audience transformation**: Client tokens arrive with audience `mcp-server`. |
| 243 | + ToolHive exchanges them for tokens with audience `backend-api`, which the |
| 244 | + backend service expects. |
| 245 | +- **Scope transformation**: The original token has MCP-specific scopes, while |
| 246 | + the exchanged token has backend-specific scopes (`api:read`, `api:write`). The |
| 247 | + user's identity is preserved, but the permissions are transformed for the |
| 248 | + target service. |
| 249 | + |
| 250 | +## Related information |
| 251 | + |
| 252 | +- [Backend authentication](../concepts/backend-auth.mdx) - conceptual overview |
| 253 | + of token exchange and federation |
| 254 | +- [Configure token exchange (CLI)](../guides-cli/token-exchange.mdx) - CLI-based |
| 255 | + setup |
| 256 | +- [Authentication and authorization](./auth-k8s.mdx) - basic auth setup for MCP |
| 257 | + servers in Kubernetes |
| 258 | +- [MCPExternalAuthConfig reference](../reference/operator/mcpexternalauthconfig.md) - |
| 259 | + complete CRD specification |
0 commit comments