Skip to content

Commit 22415f1

Browse files
jiaenrenclaude
andcommitted
Remove MCP direct identity-provider mode
MCP shipped two ways to authenticate a caller: the Gateway validating a bearer and MCP trusting injected headers, or FastMCP verifying the caller itself. Only one can be the answer, and the published documentation already recommended the second. This removes the first. request_context.py loses the header trust boundary entirely: the ASGI middleware, the per-request state, the header parsing, and the user_name field that no consumer read. get_request_credentials collapses to the FastMCP access token, which is now the only source of caller identity, so identity can no longer come from a header a caller could set. The module goes from 355 lines to 194. auth_enabled disappears with it. Authentication is not a mode any more, so its configuration is required rather than conditionally required, and a deployment that cannot reach an identity provider fails at load instead of starting up unauthenticated. The Gateway stops synthesising what FastMCP serves: the protected-resource route forwards unconditionally, and the local_reply_config that rewrote the /mcp 401 challenge is gone because FastMCP emits it. services.mcp loses authorizationServers and scopes, and oidcProxy loses its enabled toggle, which could only ever be true. Tests authenticate the way the service does. protocol_harness gains a static token verifier and a service_config helper, so suites that exercise the tool pipeline get a real auth provider instead of trusted headers. The smoke checks come with it, because deploying proxy mode for the first time showed the suite failing before it reached any assertion: it authenticates with the OSMO-issued OETF token, which FastMCP rejects. The unauthenticated surface a client uses to bootstrap OAuth needs no token and is now its own test -- the 401 challenge and its resource_metadata pointer, the RFC 9728 document, that FastMCP advertises its authorization endpoint under /mcp, and that the health endpoints the /mcp prefix would otherwise publish return 404. The authenticated catalog and workflow checks name OSMO_MCP_ACCESS_TOKEN, which runs them when an identity-provider token is supplied. Two smoke-check fixes ride along. The expected catalog was missing osmo_list_tasks, added by #1311, so its exact-equality assertion would have failed at 25 versus 26 had it ever run, and the two counts in the service README were stale for the same reason. The suite also carried an `auth` tag that the kind environment's exclude_tags dropped, so it never ran anywhere -- which is why the staleness went unnoticed. It is tagged by what it needs deployed now, with kind excluding `mcp` explicitly. Two behaviours changed and are recorded rather than hidden. An unauthenticated request is now rejected after RequestBodyLimitMiddleware has buffered its bounded body rather than before, because FastMCP's auth runs inside the route. And whether a follow-up request succeeds after a cancelled one is FastMCP's to decide; verified against the pre-change tree that an authenticated deployment already behaved this way, so this change does not introduce it. Removing the mode column from the deployment guide's table left three :widths: values against two columns, and the troubleshooting table in the getting-started guide lost a cell marker on its HTTP 403 row. Both are Sphinx errors, so docs-build failed on this commit. Fixed here rather than later in the stack: a PR should not leave the docs build red for the ones behind it. The smoke-check skip message described the wrong token. FastMCP's proxy issues its own token to the client and keeps the upstream identity-provider token server-side, validating it by JTI lookup on each request (oauth_proxy/proxy.py:1820-1848), so a raw identity-provider token does not authenticate to /mcp. The message now names the token the caller actually needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 190a69a commit 22415f1

24 files changed

Lines changed: 381 additions & 1308 deletions

File tree

deployments/charts/service/README.md

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ OSMO services write logs to standard streams for collection by the platform log
9595

9696
The optional MCP workload exposes predefined OSMO operations to compatible
9797
native or desktop MCP clients. In direct-provider mode, the Gateway validates
98-
the bearer on `/mcp`. With `oidcProxy.enabled`, FastMCP validates its own proxy
98+
the bearer on `/mcp`. FastMCP validates its own proxy
9999
token and relays the verified upstream access token through the same Gateway
100100
for each mapped OSMO API request. That API request still passes the deployment's
101101
normal identity-provider validation and semantic RBAC.
@@ -115,15 +115,12 @@ destination, but it cannot validate external DNS.
115115
| `services.mcp.imageName` | MCP image repository name. | `mcp` |
116116
| `services.mcp.imageTag` | Per-MCP image tag override; falls back to `global.osmoImageTag` when empty. | `""` |
117117
| `services.mcp.resourceUrl` | Canonical public HTTPS MCP URL ending in exact `/mcp`; also determines the fixed outbound Gateway origin. | `""` |
118-
| `services.mcp.authorizationServers` | OAuth/OIDC issuers advertised in direct-provider mode; ignored when `oidcProxy.enabled` is true. | `[]` |
119-
| `services.mcp.scopes` | OAuth scopes advertised in direct-provider mode; ignored when `oidcProxy.enabled` is true. | `[]` |
120118
| `services.mcp.allowedOrigins` | Exact browser origins permitted on `/mcp`; native clients normally omit `Origin`. | `[]` |
121119
| `services.mcp.requestTimeoutSeconds` | Total timeout for each MCP-initiated Gateway request, from 1 through 60 seconds. | `10` |
122120
| `services.mcp.replicas` | Number of MCP replicas. The OIDC proxy keeps its state in Redis, so it scales out. | `1` |
123121
| `services.mcp.extraEnv` | Additional non-managed environment variables. It cannot override MCP host, port, Gateway origin, or request timeout. | `[]` |
124122
| `services.mcp.extraVolumeMounts` | Additional MCP container volume mounts, including Vault-injected credential files. | `[]` |
125123
| `services.mcp.extraVolumes` | Additional MCP pod volumes. | `[]` |
126-
| `services.mcp.oidcProxy.enabled` | Enable FastMCP's built-in OIDC proxy inside the existing MCP process. It advertises CIMD and retains DCR as a compatibility fallback. | `false` |
127124
| `services.mcp.oidcProxy.oidc.configUrl` | Upstream OIDC discovery URL. | `""` |
128125
| `services.mcp.oidcProxy.oidc.clientId` | Administrator-managed confidential OIDC application client ID. | `""` |
129126
| `services.mcp.oidcProxy.oidc.clientSecretFile` | Mounted file containing the upstream OIDC client secret. | `/etc/osmo/mcp-auth/client-secret` |

deployments/charts/service/ci/mcp-values.yaml

Lines changed: 0 additions & 40 deletions
This file was deleted.

deployments/charts/service/ci/validate-mcp-chart.sh

Lines changed: 8 additions & 69 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,12 @@ set -euo pipefail
1919

2020
SCRIPT_DIR="$(CDPATH= cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
2121
CHART_DIR="$(dirname -- "$SCRIPT_DIR")"
22-
VALUES_FILE="$SCRIPT_DIR/mcp-values.yaml"
2322
PROXY_VALUES_FILE="$SCRIPT_DIR/mcp-oidc-proxy-values.yaml"
24-
RENDERED_MANIFEST="$(mktemp)"
25-
MCP_MANIFEST="$(mktemp)"
2623
DISABLED_MANIFEST="$(mktemp)"
2724
PROXY_RENDERED_MANIFEST="$(mktemp)"
2825
PROXY_MCP_MANIFEST="$(mktemp)"
29-
trap 'rm -f "$RENDERED_MANIFEST" "$MCP_MANIFEST" "$DISABLED_MANIFEST" \
30-
"$PROXY_RENDERED_MANIFEST" "$PROXY_MCP_MANIFEST"' EXIT
26+
trap 'rm -f "$DISABLED_MANIFEST" "$PROXY_RENDERED_MANIFEST" \
27+
"$PROXY_MCP_MANIFEST"' EXIT
3128

3229
fail() {
3330
echo "MCP chart validation failed: $*" >&2
@@ -102,14 +99,8 @@ expect_render_failure() {
10299
esac
103100
}
104101

105-
helm lint "$CHART_DIR" --values "$VALUES_FILE"
106102
helm lint "$CHART_DIR" --values "$PROXY_VALUES_FILE"
107103
helm template mcp-disabled "$CHART_DIR" >"$DISABLED_MANIFEST"
108-
helm template mcp-validation "$CHART_DIR" \
109-
--values "$VALUES_FILE" >"$RENDERED_MANIFEST"
110-
helm template mcp-validation "$CHART_DIR" \
111-
--values "$VALUES_FILE" \
112-
--show-only templates/mcp-service.yaml >"$MCP_MANIFEST"
113104
helm template mcp-proxy-validation "$CHART_DIR" \
114105
--values "$PROXY_VALUES_FILE" >"$PROXY_RENDERED_MANIFEST"
115106
helm template mcp-proxy-validation "$CHART_DIR" \
@@ -124,57 +115,10 @@ for forbidden in \
124115
assert_file_omits "$DISABLED_MANIFEST" "$forbidden"
125116
done
126117

127-
for forbidden in \
128-
'path: /.well-known/oauth-authorization-server' \
129-
'path: /authorize' \
130-
'path: /auth/callback' \
131-
'path: /register' \
132-
'path: /token' \
133-
'path: /consent'; do
134-
assert_file_omits "$RENDERED_MANIFEST" "$forbidden"
135-
done
136-
137-
for expected in \
138-
'kind: Deployment' \
139-
'kind: Service' \
140-
'name: osmo-mcp' \
141-
'image: nvcr.io/nvidia/osmo/mcp:latest' \
142-
'name: OSMO_GATEWAY_URL' \
143-
'name: OSMO_MCP_REQUEST_TIMEOUT_SECONDS' \
144-
'name: OSMO_MCP_AUTH_ENABLED' \
145-
'kind: NetworkPolicy' \
146-
'name: osmo-mcp-allow-gateway-envoy' \
147-
'app.kubernetes.io/component: envoy' \
148-
'automountServiceAccountToken: false'; do
149-
assert_file_contains "$MCP_MANIFEST" "$expected"
150-
done
151-
assert_file_contains "$RENDERED_MANIFEST" 'path: /mcp'
152-
assert_file_contains "$RENDERED_MANIFEST" 'path: /.well-known/oauth-protected-resource/mcp'
153-
assert_file_contains "$RENDERED_MANIFEST" '\"authorization_servers\":[\"https://issuer.example.com\"]'
154-
assert_file_contains "$RENDERED_MANIFEST" '\"resource\":\"https://osmo.example.com/mcp\"'
155-
assert_file_contains "$RENDERED_MANIFEST" '\"scopes_supported\":[\"mcp:Access\"]'
156-
assert_file_contains "$RENDERED_MANIFEST" 'local safe_roles = {}'
157-
assert_file_contains "$RENDERED_MANIFEST" "not string.find(role, '[,%c]')"
158-
assert_file_contains "$RENDERED_MANIFEST" "table.concat(safe_roles, ',')"
159-
assert_env_value "$MCP_MANIFEST" OSMO_GATEWAY_URL https://osmo.example.com
160-
assert_env_value "$MCP_MANIFEST" OSMO_MCP_REQUEST_TIMEOUT_SECONDS 10
161-
assert_env_value "$MCP_MANIFEST" OSMO_MCP_AUTH_ENABLED false
162-
assert_route_omits "$RENDERED_MANIFEST" osmo-mcp 'typed_per_filter_config:'
163-
164-
for expected in \
165-
'name: OSMO_MCP_AUTH_RESOURCE_URL' \
166-
'name: OSMO_MCP_AUTH_REDIS_URL' \
167-
'name: OSMO_MCP_AUTH_REDIS_CONNECT_TIMEOUT_SECONDS' \
168-
'name: OSMO_MCP_AUTH_REDIS_OPERATION_TIMEOUT_SECONDS' \
169-
'name: OSMO_MCP_AUTH_OIDC_CONFIG_URL' \
170-
'name: OSMO_MCP_AUTH_OIDC_CLIENT_ID' \
171-
'name: OSMO_MCP_AUTH_OIDC_CLIENT_SECRET_FILE' \
172-
'name: OSMO_MCP_AUTH_OIDC_ACCESS_TOKEN_ISSUER' \
173-
'name: OSMO_MCP_AUTH_OIDC_ACCESS_TOKEN_REQUIRED_SCOPE' \
174-
'name: OSMO_MCP_AUTH_UPSTREAM_TIMEOUT_SECONDS' \
175-
'secretName: mcp-oidc-proxy-secrets'; do
176-
assert_file_contains "$PROXY_MCP_MANIFEST" "$expected"
177-
done
118+
# The roles Lua filter has no other coverage in the repo.
119+
assert_file_contains "$PROXY_RENDERED_MANIFEST" 'local safe_roles = {}'
120+
assert_file_contains "$PROXY_RENDERED_MANIFEST" "not string.find(role, '[,%c]')"
121+
assert_file_contains "$PROXY_RENDERED_MANIFEST" "table.concat(safe_roles, ',')"
178122

179123
# FastMCP's OAuth endpoints are published under /mcp, so the gateway needs one
180124
# prefix route rather than an entry per endpoint name.
@@ -245,21 +189,16 @@ if awk -v secret_name='mcp-oidc-proxy-secrets' '
245189
fail 'OIDC proxy mode rendered credential material into a Secret'
246190
fi
247191

248-
expect_render_failure "$VALUES_FILE" \
192+
expect_render_failure "$PROXY_VALUES_FILE" \
249193
'encoded alternate Gateway origin' \
250194
'services.mcp.resourceUrl must be a valid HTTPS origin' \
251195
--set 'services.mcp.resourceUrl=https://osmo.example.com%40evil.example.com/mcp'
252196

253-
expect_render_failure "$VALUES_FILE" \
197+
expect_render_failure "$PROXY_VALUES_FILE" \
254198
'managed Gateway URL override' \
255199
'services.mcp.extraEnv must not override managed variable OSMO_GATEWAY_URL' \
256200
--set-json 'services.mcp.extraEnv=[{"name":"OSMO_GATEWAY_URL","value":"https://evil.example.com"}]'
257201

258-
expect_render_failure "$PROXY_VALUES_FILE" \
259-
'OIDC proxy without MCP' \
260-
'services.mcp.oidcProxy.enabled requires services.mcp.enabled=true' \
261-
--set 'services.mcp.enabled=false'
262-
263202
expect_render_failure "$PROXY_VALUES_FILE" \
264203
'OIDC proxy Redis database below range' \
265204
'services.mcp.oidcProxy.redis.dbNumber must be between 0 and 15' \

deployments/charts/service/templates/_gateway-envoy-config.tpl

Lines changed: 1 addition & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -31,16 +31,11 @@ setting detects this rotation and triggers Envoy to reload.
3131
{{- $mcp := .Values.services.mcp }}
3232
{{- $mcpEnabled := $mcp.enabled | default false }}
3333
{{- $mcpOidcProxy := $mcp.oidcProxy }}
34-
{{- if and ($mcpOidcProxy.enabled | default false) (not $mcpEnabled) }}
35-
{{- fail "services.mcp.oidcProxy.enabled requires services.mcp.enabled=true" }}
36-
{{- end }}
37-
{{- $mcpOidcProxyEnabled := and $mcpEnabled ($mcpOidcProxy.enabled | default false) }}
34+
{{- $mcpOidcProxyEnabled := $mcpEnabled }}
3835
{{- $mcpPath := "/mcp" }}
3936
{{- $mcpMetadataPath := "/.well-known/oauth-protected-resource/mcp" }}
4037
{{- $mcpResourceUrl := "" }}
4138
{{- $mcpMetadataUrl := "" }}
42-
{{- $mcpAuthorizationServers := $mcp.authorizationServers }}
43-
{{- $mcpScopes := $mcp.scopes }}
4439
{{- $skipAuthPaths := concat (default (list) $envoy.skipAuthPaths) (default (list) $envoy.extraSkipAuthPaths) }}
4540
{{- $authnSkipPaths := $skipAuthPaths }}
4641
{{- if $gw.oauth2Proxy.enabled }}
@@ -54,28 +49,6 @@ setting detects this rotation and triggers Envoy to reload.
5449
{{- fail "services.mcp.enabled requires gateway.authz.enabled=true" }}
5550
{{- end }}
5651
{{- $mcpResourceUrl = include "osmo.mcp-resource-url" . }}
57-
{{- if not $mcpOidcProxyEnabled }}
58-
{{- if not (kindIs "slice" $mcp.authorizationServers) }}
59-
{{- fail "services.mcp.authorizationServers must be a list" }}
60-
{{- end }}
61-
{{- if eq (len $mcp.authorizationServers) 0 }}
62-
{{- fail "services.mcp.authorizationServers must contain at least one issuer when MCP is enabled" }}
63-
{{- end }}
64-
{{- if not (kindIs "slice" $mcp.scopes) }}
65-
{{- fail "services.mcp.scopes must be a list" }}
66-
{{- end }}
67-
{{- if eq (len $mcp.scopes) 0 }}
68-
{{- fail "services.mcp.scopes must contain at least one scope when MCP is enabled" }}
69-
{{- end }}
70-
{{- range $scope := $mcp.scopes }}
71-
{{- if not (kindIs "string" $scope) }}
72-
{{- fail "services.mcp.scopes entries must be strings" }}
73-
{{- end }}
74-
{{- if eq (trim $scope) "" }}
75-
{{- fail "services.mcp.scopes entries must not be empty" }}
76-
{{- end }}
77-
{{- end }}
78-
{{- end }}
7952
{{- if not $envoy.jwt.providers }}
8053
{{- fail "services.mcp.enabled requires at least one gateway.envoy.jwt.providers entry" }}
8154
{{- end }}
@@ -299,27 +272,9 @@ data:
299272
- name: ":method"
300273
string_match:
301274
exact: GET
302-
{{- if $mcpOidcProxyEnabled }}
303275
route:
304276
cluster: osmo-mcp
305277
timeout: 15s
306-
{{- else }}
307-
direct_response:
308-
status: 200
309-
body:
310-
inline_string: {{ dict "resource" $mcpResourceUrl "authorization_servers" $mcpAuthorizationServers "bearer_methods_supported" (list "header") "scopes_supported" $mcpScopes | toJson | quote }}
311-
{{- end }}
312-
response_headers_to_add:
313-
- header:
314-
key: content-type
315-
value: application/json
316-
append_action: OVERWRITE_IF_EXISTS_OR_ADD
317-
# Inspector completes OAuth in its browser UI and fetches
318-
# this public, non-secret discovery document from localhost.
319-
- header:
320-
key: access-control-allow-origin
321-
value: "*"
322-
append_action: OVERWRITE_IF_EXISTS_OR_ADD
323278
typed_per_filter_config:
324279
{{- include "osmo.gateway-auth-filters-disabled" . | nindent 20 }}
325280

@@ -471,35 +426,6 @@ data:
471426
{{- end }}
472427
{{- end }}
473428

474-
{{- if $mcpEnabled }}
475-
# jwt_authn emits an Envoy local 401 before the router runs. Replace
476-
# its generic bearer challenge only for the exact MCP endpoint so
477-
# standards-compatible clients can discover RFC 9728 metadata.
478-
local_reply_config:
479-
mappers:
480-
- filter:
481-
and_filter:
482-
filters:
483-
- status_code_filter:
484-
comparison:
485-
op: EQ
486-
value:
487-
default_value: 401
488-
runtime_key: osmo.mcp.jwt_unauthorized_status
489-
- header_filter:
490-
header:
491-
name: ":path"
492-
string_match:
493-
safe_regex:
494-
google_re2: {}
495-
regex: "^/mcp([?].*)?$"
496-
headers_to_add:
497-
- header:
498-
key: www-authenticate
499-
value: {{ printf "Bearer resource_metadata=%q" $mcpMetadataUrl | quote }}
500-
append_action: OVERWRITE_IF_EXISTS_OR_ADD
501-
{{- end }}
502-
503429
upgrade_configs:
504430
- upgrade_type: websocket
505431
enabled: true

deployments/charts/service/values.yaml

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -177,18 +177,6 @@ services:
177177
##
178178
requestTimeoutSeconds: 10
179179

180-
## OAuth authorization-server issuer identifiers advertised by the public
181-
## protected-resource metadata document. Configure the issuer for the
182-
## separately registered public/native MCP OAuth client.
183-
##
184-
authorizationServers: []
185-
186-
## OAuth scopes requested by MCP clients. These values are advertised as
187-
## scopes_supported in RFC 9728 protected-resource metadata and should
188-
## include the delegated scope required to access this MCP resource.
189-
##
190-
scopes: []
191-
192180
## Browser Origins permitted to call /mcp. Native MCP clients normally omit
193181
## Origin and remain allowed. When this list is empty, any present Origin is
194182
## rejected with 403.
@@ -202,8 +190,6 @@ services:
202190
## identity-provider access token to the OSMO Gateway for normal RBAC.
203191
##
204192
oidcProxy:
205-
enabled: false
206-
207193
## Upstream OpenID Connect provider. Register the fixed redirect URL
208194
## <resourceUrl origin>/mcp/auth/callback on this confidential application.
209195
##

0 commit comments

Comments
 (0)