Skip to content

Commit 7665460

Browse files
authored
Design proposal: Token exchange to acquire tokens for external auth (#2063)
1 parent d9498fe commit 7665460

File tree

1 file changed

+163
-0
lines changed

1 file changed

+163
-0
lines changed
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Token exchange in thv proxy
2+
3+
Enhancing thv proxy so that it is able to exchange the incoming token using RFC-8693 for a token that's forwarded to the back end.
4+
5+
## Problem statement
6+
7+
Per the MCP spec, the OAuth token used to authorize the access to the MCP server must be issued for the MCP server. However, the MCP server is often exposing an API where access is also authorized using OAuth \- this means that the MCP server must acquire a token meant for the backend service.
8+
9+
## Proposed solution
10+
11+
In case both the MCP server and the backend service require tickets issued by the same IDP and the IDP supports [RFC 8693](https://datatracker.ietf.org/doc/html/rfc8693) token exchange, we can exchange the internal token for an external one.
12+
13+
Note that other cases where the IDP represent different identity realms will be tackled separately.
14+
15+
## High-level design
16+
17+
The design can be illustrated with a flow diagram:
18+
19+
```mermaid
20+
sequenceDiagram
21+
participant Client
22+
participant AuthMW as Auth Middleware
23+
participant TEMW as Token Exchange<br/>Middleware
24+
participant OAuth as OAuth Server
25+
participant Upstream as Upstream Service
26+
27+
Client->>AuthMW: HTTP Request<br/>Authorization: Bearer token-A<br/>(aud=proxy)
28+
29+
Note over AuthMW: Validate token-A signature,<br/>expiry, audience
30+
AuthMW->>AuthMW: Extract JWT claims
31+
AuthMW->>TEMW: Request + Claims Context
32+
33+
Note over TEMW: Extract token-A from<br/>Authorization header
34+
35+
TEMW->>OAuth: POST /token<br/>grant_type=token-exchange<br/>subject_token=token-A<br/>audience=upstream<br/>client_id=...<br/>client_secret=...
36+
37+
Note over OAuth: Validate token-A<br/>Check client permissions<br/>Issue new token
38+
39+
OAuth-->>TEMW: Response<br/>access_token=token-B<br/>(aud=upstream)<br/>expires_in=3600
40+
41+
alt Replace Strategy (default)
42+
Note over TEMW: Replace Authorization header
43+
TEMW->>Upstream: HTTP Request<br/>Authorization: Bearer token-B
44+
else Custom Header Strategy
45+
Note over TEMW: Add custom header,<br/>preserve original
46+
TEMW->>Upstream: HTTP Request<br/>Authorization: Bearer token-A<br/>X-Upstream-Token: Bearer token-B
47+
end
48+
49+
Note over Upstream: Validate token-B<br/>(aud=upstream)
50+
Upstream-->>TEMW: HTTP Response
51+
TEMW-->>AuthMW: HTTP Response
52+
AuthMW-->>Client: HTTP Response
53+
54+
Note over Client,Upstream: Token exchange transparent to client
55+
```
56+
57+
An important note is that for the token exchange to work, the MCP server or rather the proxy must have a client ID and often (depending on the IDP configuration) also a client secret.
58+
59+
## Implementation details
60+
61+
The core of the implementation is a new Token Exchange middleware. The middleware will use a Go module that will implement the exchange wrapped in the standard TokenSource interface. This will allow for composability with existing patterns to cache tokens such as ReuseTokenSource
62+
63+
The new middleware will be injected after the auth middleware to make sure the token authorizing access to the MCP server is validated.
64+
65+
Once the back end API token is acquired, the token is either injected into the Authorization: Bearer header or a custom header.
66+
67+
In the first PR, we'll run the token exchange for each request. This does not scale and needs to be addressed in subsequent patches.
68+
69+
## Usage examples
70+
71+
```shell
72+
thv proxy my-mcp-server \
73+
--oidc-issuer https://keycloak.example.com/realms/myrealm \
74+
--oidc-client-id proxy-client \
75+
--oidc-client-secret proxy-secret \
76+
--token-exchange-url https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token \
77+
--token-exchange-client-id exchange-client \
78+
--token-exchange-client-secret exchange-secret \
79+
--token-exchange-audience backend-service
80+
```
81+
82+
```shell
83+
thv run my-mcp-server \
84+
--oidc-issuer https://keycloak.example.com/realms/myrealm \
85+
--oidc-client-id mcp-client \
86+
--token-exchange-url https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token \
87+
--token-exchange-client-id exchange-client \
88+
--token-exchange-client-secret exchange-secret \
89+
--token-exchange-audience upstream-api
90+
```
91+
92+
## Operator integration
93+
94+
For Kubernetes deployments, token exchange configuration is exposed through the `MCPServer` CRD via the `externalAuthConfig` field.
95+
96+
### CRD structure
97+
98+
```go
99+
type ExternalAuthConfig struct {
100+
Type string `json:"type"` // "tokenExchange" for now
101+
TokenExchange *TokenExchangeConfig `json:"tokenExchange,omitempty"`
102+
}
103+
104+
type TokenExchangeConfig struct {
105+
Type string `json:"type"` // "inline" or "configMap"
106+
Inline *InlineTokenExchangeConfig `json:"inline,omitempty"`
107+
ConfigMap *ConfigMapTokenExchangeRef `json:"configMap,omitempty"`
108+
}
109+
110+
type InlineTokenExchangeConfig struct {
111+
TokenURL string `json:"tokenUrl"`
112+
ClientID string `json:"clientId"`
113+
ClientSecretRef *SecretKeyRef `json:"clientSecretRef,omitempty"`
114+
Audience string `json:"audience,omitempty"`
115+
Scopes string `json:"scopes,omitempty"`
116+
ExternalTokenHeaderName string `json:"externalTokenHeaderName,omitempty"`
117+
}
118+
```
119+
120+
### Example
121+
122+
```yaml
123+
apiVersion: toolhive.stacklok.dev/v1alpha1
124+
kind: MCPServer
125+
metadata:
126+
name: api-proxy
127+
spec:
128+
image: ghcr.io/my-org/mcp-server:latest
129+
130+
oidcConfig:
131+
type: kubernetes
132+
kubernetes:
133+
audience: toolhive
134+
135+
externalAuthConfig:
136+
type: tokenExchange
137+
tokenExchange:
138+
type: inline
139+
inline:
140+
tokenUrl: https://keycloak.example.com/realms/myrealm/protocol/openid-connect/token
141+
clientId: exchange-client
142+
clientSecretRef:
143+
name: token-exchange-creds
144+
key: client-secret
145+
audience: backend-service
146+
```
147+
148+
### Flow
149+
150+
1. User creates `MCPServer` CR with `backendTokenConfig`
151+
2. Operator reconciles and generates deployment with appropriate CLI flags or RunConfig
152+
3. ProxyRunner starts with token exchange middleware configured
153+
4. Requests flow through authentication → token exchange → upstream proxy
154+
155+
## Future Enhancements
156+
157+
- **Federated Identity Token Acquisition**: Support token exchange when external IDPs have federation established with the internal IDP (e.g., Google's Workforce Identity Federation, GitHub Apps) - requires one-time federation setup and identity mapping but provides full auditability and automatic token acquisition
158+
159+
- **OAuth Flow for Non-Federated IDPs**: Implement a "two-headed OAuth proxy" component that drives OAuth flows against external IDPs where no federation exists - stores and refreshes per-user tokens securely to minimize repeated authentication
160+
161+
- **Network Wrapper for Generic MCP Servers**: Build an egress/ingress interceptor that wraps unmodified MCP servers (those only supporting single API keys) to inject per-call credentials by intercepting outgoing HTTP requests and adding authentication headers
162+
163+
- **Per-User Token Storage and Refresh**: Create secure token storage mechanism with automatic refresh capabilities to maintain long-lived sessions without repeated user authentication

0 commit comments

Comments
 (0)