This is an automated email from the ASF dual-hosted git repository.
epugh pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr-mcp.git
The following commit(s) were added to refs/heads/main by this push:
new 0aa377c docs(security): document HTTP transport security model (#126)
0aa377c is described below
commit 0aa377c56407d9bb835c11968b91f4b76ab9d233
Author: Aditya Parikh <[email protected]>
AuthorDate: Fri May 8 16:43:26 2026 -0400
docs(security): document HTTP transport security model (#126)
Adds docs/security/http.md as the companion to docs/security/stdio.md.
Documents the secured filter chain, JWT validation rules, audience
binding via RFC 8707 resource indicators, CORS allowlist, actuator
exposure, and per-IdP setup notes (Auth0, Okta, Keycloak — including
Keycloak's RFC 8707 limitation and Audience-mapper workaround).
---------
Signed-off-by: adityamparikh <[email protected]>
Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]>
---
docs/security/http.md | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 176 insertions(+)
diff --git a/docs/security/http.md b/docs/security/http.md
new file mode 100644
index 0000000..7c82324
--- /dev/null
+++ b/docs/security/http.md
@@ -0,0 +1,176 @@
+# HTTP Transport — Security Model
+
+This document captures the security posture of the Solr MCP server when run in
+**HTTP mode** (`PROFILES=http`). Companion to [`stdio.md`](./stdio.md), which
+covers the default STDIO transport.
+
+## TL;DR
+
+HTTP mode is **secured by default**: the OAuth2 filter chain enforces
+authentication on every MCP tool call and every actuator endpoint except
+`/actuator/health`. Operators must configure an OAuth2 authorization server.
+The MCP Authorization specification mandates this — STDIO is the only transport
+that legitimately runs without auth.
+
+## Trust model
+
+| | STDIO | HTTP |
+|---|---|---|
+| Network listener | None | Servlet container on configured port |
+| Trust boundary | OS user that launched the process | OAuth2 access token
(JWT bearer) per request |
+| Auth required | No (per [MCP Authorization
spec](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization))
| Yes (per same spec) |
+| Default in this codebase | Active | Active when `PROFILES=http` and
`http.security.enabled=true` (default) |
+
+## Configuration knobs
+
+| Property | Env var | Default | Effect |
+|---|---|---|---|
+| `http.security.enabled` | `HTTP_SECURITY_ENABLED` | `true` | When `false`,
the unsecured filter chain is active and every MCP/actuator endpoint is
anonymous. Use only for local development. |
+| `spring.security.oauth2.resourceserver.jwt.issuer-uri` | `OAUTH2_ISSUER_URI`
| `https://your-auth0-domain.auth0.com/` (placeholder — replace) | OpenID
Provider issuer URL. Used to fetch JWKS for signature validation. The MCP
server fails to start if this URL is unreachable. |
+| `mcp.cors.allowed-origins` | `MCP_CORS_ALLOWED_ORIGINS` |
`http://localhost:6274,http://127.0.0.1:6274` | Explicit CORS allowlist.
Wildcards are rejected because the filter chain uses credentials. |
+| `solr.url` | `SOLR_URL` | `http://localhost:8983/solr/` | Same as STDIO. |
+
+## Security architecture
+
+### 1. Filter chain (`HttpSecurityConfiguration`)
+
+```java
+http.authorizeHttpRequests(auth -> {
+ auth.requestMatchers("/actuator/health").permitAll();
+ auth.requestMatchers("/actuator", "/actuator/**").authenticated();
+ auth.requestMatchers("/mcp").permitAll(); // gated by @PreAuthorize, see
below
+ auth.anyRequest().authenticated();
+})
+.with(McpServerOAuth2Configurer.mcpServerOAuth2(),
+ cfg -> cfg.authorizationServer(issuerUrl)
+ .resourcePath("/mcp")
+ .validateAudienceClaim(true))
+.cors(...).csrf(CsrfConfigurer::disable);
+```
+
+- `/mcp` is permitted at the HTTP layer because Spring AI MCP routes the entire
+ JSON-RPC stream through one path. **Per-tool authorization is enforced via
+ `@PreAuthorize("isAuthenticated()")` on every `@McpTool` method.** This is
+ the canonical pattern from
+ [`spring-ai-community/mcp-security` "secured tools"
sample](https://github.com/spring-ai-community/mcp-security/blob/main/samples/sample-mcp-server-secured-tools/src/main/java/org/springaicommunity/mcp/security/sample/server/securedtools/HistoricalWeatherService.java).
+- `/actuator/health` stays anonymous so load balancers and orchestrators can
+ probe liveness. Everything else under `/actuator` requires auth so an
+ unauthenticated caller cannot read the SBOM, scrape Prometheus metrics that
+ map the tool surface, or change log levels.
+- CSRF is disabled because the API is stateless Bearer-token: no cookies, no
+ session, no auto-attached credentials by browsers. See [Spring Security —
+ When to use CSRF
protection](https://docs.spring.io/spring-security/reference/features/exploits/csrf.html#csrf-when).
+
+### 2. JWT validation
+
+The `mcpServerOAuth2()` configurer wires a Spring Security
+`JwtDecoder` that validates:
+
+1. **Signature** against the JWKS fetched from
`issuer-uri`/`.well-known/openid-configuration`.
+2. **Issuer** matches the configured `issuer-uri`.
+3. **Expiration** (`exp`) and not-before (`nbf`).
+4. **Audience** (`aud`) matches the canonical resource indicator declared by
+ `resourcePath("/mcp")` — per
+ [RFC 8707 Resource Indicators](https://www.rfc-editor.org/rfc/rfc8707.html)
+ and
+ [the MCP Authorization spec's Token Audience Binding
requirement](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization).
+
+Without audience validation, any valid JWT from the same IdP issued for any
+sibling application would be accepted (CWE-345).
+
+### 3. Per-IdP setup for the audience claim
+
+The MCP server requires the JWT to carry an `aud` claim matching the canonical
+resource URI. Per IdP:
+
+| IdP | How to populate `aud` |
+|---|---|
+| **Auth0** | Pass `audience=<MCP server URL>` on the auth request. Auth0
reflects it into `aud` automatically. Configure the API in the Auth0 dashboard
with the same identifier. [Auth0
docs](https://auth0.com/docs/secure/tokens/access-tokens). |
+| **Okta** | Configure the audience on the Authorization Server (`Security →
API → Authorization Servers → Settings`). Tokens issued from that AS will carry
the configured `aud`. |
+| **Keycloak** | Keycloak does **not** yet honor RFC 8707 `resource=` natively
(see [Keycloak issue
#41526](https://github.com/keycloak/keycloak/issues/41526)). Workaround: add an
**Audience** protocol mapper on a client scope, set `Included Custom Audience`
to the MCP server URL, and assign that client scope as a default scope on the
MCP client. [Keycloak MCP integration
docs](https://www.keycloak.org/securing-apps/mcp-authz-server). |
+
+### 4. CORS
+
+The CORS allowlist is intentionally narrow:
+
+- **Origins**: explicit allowlist (default: MCP Inspector's local proxy at
+ `http://localhost:6274`). Wildcards forbidden — combining `*` origin with
+ credentials is the [classic CWE-942
misconfiguration](https://cwe.mitre.org/data/definitions/942.html).
+- **Methods**: `GET, POST, DELETE, OPTIONS` — the methods used by the MCP
+ Streamable HTTP transport.
+- **Headers**: `Authorization, Content-Type, Mcp-Session-Id,
MCP-Protocol-Version, Last-Event-ID`.
+- **Credentials**: allowed (Bearer-token flows).
+
+Add origins via `MCP_CORS_ALLOWED_ORIGINS` (comma-separated). Real production
+MCP clients (Claude Desktop, Spring AI MCP client, etc.) speak HTTP from a
+backend or native process and don't trigger CORS preflights — the allowlist
+exists for browser-based tooling.
+
+## Operational guidance
+
+### Required for production
+
+1. **Set a real `OAUTH2_ISSUER_URI`** pointing at your authorization server.
+ The placeholder default fails to start.
+2. **Configure your IdP to populate `aud`** with the MCP server's URL (see
+ table above).
+3. **Bind to a private network or behind an authenticated ingress**. The MCP
+ transport spec recommends localhost binding for local servers and
+ authentication for everything else.
+
+### Forbidden
+
+- `mcp.cors.allowed-origins=*` together with `allowCredentials=true`.
+- `http.security.enabled=false` on a network-reachable deployment.
+- Passing `SOLR_URL` from MCP tool input — it must come from
deployer-controlled
+ environment.
+
+## Primary sources
+
+### MCP specification (2025-06-18)
+
+- [MCP
Authorization](https://modelcontextprotocol.io/specification/2025-06-18/basic/authorization)
— OAuth2 resource server requirements, audience binding, token validation
rules.
+- [MCP Transports — Streamable HTTP
security](https://modelcontextprotocol.io/specification/2025-06-18/basic/transports#streamable-http)
— Origin validation, localhost binding for local deployments, authentication
requirements.
+- [MCP Security Best
Practices](https://modelcontextprotocol.io/specification/2025-06-18/basic/security_best_practices)
— Confused deputy, token passthrough prohibition, SSRF, session hijacking.
+
+### Spring AI / Spring AI Community
+
+- [Spring AI MCP Security
reference](https://docs.spring.io/spring-ai/reference/api/mcp/mcp-security.html)
+- [`spring-ai-community/mcp-security`
README](https://github.com/spring-ai-community/mcp-security)
+- [Sample: secured-tools
`McpServerConfiguration`](https://github.com/spring-ai-community/mcp-security/blob/main/samples/sample-mcp-server-secured-tools/src/main/java/org/springaicommunity/mcp/security/sample/server/securedtools/McpServerConfiguration.java)
+- [Spring Blog — *Securing Spring AI MCP servers with OAuth2*
(2025-04-02)](https://spring.io/blog/2025/04/02/mcp-server-oauth2/)
+- [Spring Blog — *Securing MCP Servers with Spring AI*
(2025-09-30)](https://spring.io/blog/2025/09/30/spring-ai-mcp-server-security/)
+
+### Spring Security / Spring Boot
+
+- [Spring Security — OAuth2 Resource Server /
JWT](https://docs.spring.io/spring-security/reference/servlet/oauth2/resource-server/jwt.html)
+- [Spring Security — CSRF
reference](https://docs.spring.io/spring-security/reference/features/exploits/csrf.html)
+- [Spring Boot — Actuator
Endpoints](https://docs.spring.io/spring-boot/reference/actuator/endpoints.html)
+
+### IdP-specific
+
+- [Auth0 — Validate JSON Web
Tokens](https://auth0.com/docs/secure/tokens/json-web-tokens/validate-json-web-tokens)
+- [Keycloak — Integrating with Model Context
Protocol](https://www.keycloak.org/securing-apps/mcp-authz-server)
+- [Keycloak Issue #41526 — RFC 8707 resource parameter for
MCP](https://github.com/keycloak/keycloak/issues/41526)
+
+### Standards
+
+- [RFC 7519 — JSON Web Token
(JWT)](https://datatracker.ietf.org/doc/html/rfc7519)
+- [RFC 6750 — OAuth 2.0 Bearer Token
Usage](https://datatracker.ietf.org/doc/html/rfc6750)
+- [RFC 8707 — Resource Indicators for OAuth
2.0](https://www.rfc-editor.org/rfc/rfc8707)
+- [RFC 9728 — OAuth 2.0 Protected Resource
Metadata](https://datatracker.ietf.org/doc/html/rfc9728)
+
+### CWE / OWASP
+
+- [CWE-306 (Missing Authentication for Critical
Function)](https://cwe.mitre.org/data/definitions/306.html)
+- [CWE-345 (Insufficient Verification of Data
Authenticity)](https://cwe.mitre.org/data/definitions/345.html)
+- [CWE-732 (Incorrect Permission
Assignment)](https://cwe.mitre.org/data/definitions/732.html)
+- [CWE-942 (Permissive Cross-domain
Policy)](https://cwe.mitre.org/data/definitions/942.html)
+- [CWE-943 (Query Logic
Injection)](https://cwe.mitre.org/data/definitions/943.html)
+- [OWASP API Security Top 10
(2023)](https://owasp.org/API-Security/editions/2023/en/0x00-header/)
+
+## Related documents
+
+- [STDIO transport security model](./stdio.md)
+- [GraalVM native image spec](../specs/graalvm-native-image.md)
+- [Logging architecture in `CLAUDE.md`](../../CLAUDE.md#logging-architecture)