Skip to content

docs(security): make the HTTP + OAuth2 setup walk end to end - #211

Open
adityamparikh wants to merge 2 commits into
apache:mainfrom
adityamparikh:docs/http-security-walkthrough
Open

adityamparikh wants to merge 2 commits into
apache:mainfrom
adityamparikh:docs/http-security-walkthrough

Conversation

@adityamparikh

@adityamparikh adityamparikh commented Sep 16, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Someone following the security guides could configure an IdP, start the server in HTTP mode with security on, and still be unable to call a single tool from an MCP client. Two facts the docs got wrong or left out:

  1. The audience is derived from the request, not fixed. McpServerOAuth2Configurer builds the expected aud per request from the scheme, host and port the client used plus /mcp. The Auth0 guide and the site page told readers to use https://solr-mcp-api as the API identifier, which produces tokens the server rejects with The aud claim is not valid. The Auth0 guide also stated that no audience validation is performed, listed a login/oauth2/code/auth0 callback that a resource server does not have, and showed a stale issuer default.
  2. The server never answers an anonymous /mcp request with 401. /mcp is permitAll and @PreAuthorize denies inside each tool with an HTTP 200 Access Denied result. Claude Code, the MCP Inspector and mcp-remote start OAuth only on a 401/403, so the client guides' "handles the OAuth2 flow automatically" never happens: the client reports Connected, lists every tool, and every call is denied, with no browser opening. The path that works is a bearer header.

Changes

  • docs/security/http.md: how the audience is derived and how to read it from the running server; a new Connecting an MCP client to a secured server section with the observed status matrix and the header syntax for Claude Code, Inspector (CLI and web), mcp-remote, VS Code, Cursor and Zed; corrected production guidance for the empty issuer default.
  • docs/security/keycloak.md: audience spelling note (localhost vs 127.0.0.1); a Connecting an MCP Client section that raises the 300 s token lifetime for a trial, gives the Claude Code and Inspector commands, and explains that claude mcp list says Connected either way so only a tool call proves the gate.
  • docs/security/auth0.md: rewritten around the resource-URI identifier, with run, verify and connect steps; phantom callback and the "no audience validation" claim removed.
  • docs/clients/*.md and the docs/site/.../clients/*.md mirror: the automatic-OAuth sentences replaced with header configuration per client (VS Code inputs + headers, Cursor ${env:…}, Claude Desktop and JetBrains via mcp-remote --header, Inspector --header).
  • docs/site/content/pages/mcp/security.md, dev-docs/DEPLOYMENT.md, scripts/get-auth0-token.sh comments: brought in line.

Verification

Run on 2026-09-16 against Keycloak 26.0 (realm created with the guide's Quick Start block), Solr 9 and Claude Code 2.1.273, server started from the fat JAR with PROFILES=http HTTP_SECURITY_ENABLED=true OAUTH2_ISSUER_URI=http://localhost:8180/realms/solr-mcp:

Check Result
Anonymous initialize, tools/list 200
Anonymous tools/call list-collections 200, Access Denied, isError: true
With token, tools/call list-collections ["books","films"]
Same token sent to http://127.0.0.1:8080/mcp 401 The aud claim is not valid
Garbage token on tools/list 401 Malformed token
/actuator/metrics with / without token 200 / 401
Inspector CLI --header / without tool result / Access Denied
Claude Code claude mcp add … --header, then a list-collections call via claude -p ["books","films"]
Claude Code with the URL only ✔ Connected, tool call returns Access Denied, no OAuth prompt
PUT /admin/realms/solr-mcp {"accessTokenLifespan":3600} 204, next token exp - iat = 3600

Re-run against an Auth0 tenant on the same day: the dashboard accepts http://localhost:8080/mcp as an API Identifier, the client-credentials token carries iss with a trailing slash, aud equal to the identifier and a 24 h lifetime, and the same matrix holds (anonymous call denied, token call returns the collections, 127.0.0.1 rejected on aud, actuator 200/401, Inspector CLI and Claude Code with --header both succeed).

Second commit: scripts/get-auth0-token.sh --quiet printed an [INFO] line to stdout before the token when .env was present, so the documented TOKEN=$(... --quiet) captured the log line. Status output now goes to stderr.

Interactive browser login (claude mcp login, the Inspector's OAuth flow) needs an IdP client registration and is stated as out of scope rather than described.

./gradlew build (JDK 25): 421 tests, 0 failures, 0 skipped; rat and spotlessCheck pass (re-run after the script change, same result).

Overlap with open PRs

🤖 Generated with Claude Code

adityamparikh and others added 2 commits September 24, 2026 10:40
Someone following the security guides could configure an IdP, start the
server and still be unable to call a tool from any MCP client. Two facts
the docs got wrong or left out:

- The audience the server validates is not a fixed string. mcp-server-
  security builds it per request from the scheme, host and port the client
  used plus /mcp, so aud must contain the exact URL the client dials.
  The Auth0 guide (and the site mirror) told readers to use
  https://solr-mcp-api, which every token then fails with
  "The aud claim is not valid". The guide also claimed no audience
  validation is performed, listed a login/oauth2/code callback the
  resource server does not have, and showed a stale issuer default.
- The server never answers an anonymous /mcp request with 401: the
  handshake is permitAll and @PreAuthorize denies inside each tool with an
  HTTP 200 "Access Denied" result. Claude Code, the MCP Inspector and
  mcp-remote start OAuth only on a 401, so the client guides' "handles the
  OAuth2 flow automatically" never happens; the client reports connected
  and every call is denied. The verified path is a bearer header.

Changes:
- http.md: how the audience is derived, a "Connecting an MCP client"
  section with the observed status matrix and the header syntax per
  client, corrected production guidance for the empty issuer default.
- keycloak.md: audience spelling note, a "Connecting an MCP Client"
  section (raise the 300 s token lifetime, Claude Code and Inspector
  commands, what "Connected" does and does not prove).
- auth0.md: rewritten around the resource-URI identifier, with run,
  verify and connect steps.
- Client guides (repo and site mirror): replace the automatic-OAuth
  claims with the header configuration for Claude Code, Claude Desktop
  and JetBrains via mcp-remote, VS Code, Cursor and the Inspector.
- Site security page, DEPLOYMENT.md and the Auth0 token script comments
  brought in line.

Verified on 2026-09-16 against Keycloak 26.0, Solr 9 and Claude Code
2.1.273: curl, the Inspector CLI and Claude Code with --header all call
list-collections; the same token sent to 127.0.0.1 is rejected on aud;
without a header Claude Code shows Connected and returns Access Denied.
The Auth0 steps follow the same server-side rules but were not re-run
against a live tenant.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
…et yields only the token

With .env present the script printed "[INFO] Loading configuration from
.env file" before --quiet was parsed, so TOKEN=$(... --quiet) as the docs
show captured the log line ahead of the token. Route all status output to
stderr; stdout now carries the token alone.

Also records that the HTTP client-connection matrix was re-verified against
an Auth0 tenant: the dashboard accepts http://localhost:8080/mcp as an API
identifier, and the resulting client-credentials token (iss with trailing
slash, aud = the identifier, 24 h lifetime) passes the server's checks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Signed-off-by: Aditya Parikh <aditya.m.parikh@gmail.com>
@adityamparikh
adityamparikh force-pushed the docs/http-security-walkthrough branch from 42f5209 to ede695d Compare September 24, 2026 14:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant