Skip to content

[Bug]: mcp-auth breaks MCP client auth sessions: GET/DELETE are not gated, and PRM publishes empty scopes_supported #2869

Description

@RakhithaRR

Please select the area the issue is related to

Gateway

Please select the aspect the issue is related to

Aspect/AI (AI/LLM integration, MCP, AI readiness)

Description

Two defects in mcp-auth each cause an MCP client to discard its entire OAuth session immediately after a successful initialize and stop the server. The user-visible symptom is a connect → re-authenticate → fail loop, even though authentication itself succeeded and initialize returned 200.

Both are instances of the same client behaviour, which is worth stating up front because it makes each defect much more severe than it first looks:

An MCP client treats any 401 on the MCP endpoint as "the access token is invalid". It then invalidates its whole auth session, sends every subsequent request without a token (which the gateway also 401s), and tears the connection down. The first 401 is the cause; every later failure is a consequence.

Verified with VS Code 1.128.1 against the GitHub remote MCP server (https://api.githubcopilot.com/mcp/) proxied by the gateway, with mcp-auth (Keycloak key manager) plus an interceptor-service that injects a per-user upstream credential. Claude Code is unaffected by defect 1 because it does not send a session DELETE.


Defect 1 — mcp-auth does not gate GET or DELETE

The MCP Streamable HTTP transport has clients issue, in addition to JSON-RPC POSTs:

  • GET <mcp-endpoint> — the standalone asynchronous-notification (SSE) stream
  • DELETE <mcp-endpoint> — session termination

mcp-auth does not apply authentication to either method, so both pass straight through to the rest of the policy chain with no authenticated context established. (VS Code additionally sends the GET with no Authorization header at all, so there is nothing for a downstream policy to key off even opportunistically.)

The practical consequence: any backend-auth policy that fails closed — for example an interceptor-service that exchanges the caller's token for an upstream credential — has no identity to work with on these two requests and returns 401. That 401 then destroys the client's auth session per the behaviour above, and the MCP session dies right after initialize.

Observed on the gateway (interceptor-service returning 401 because no identity reached it):

POST   /github/mcp   status=200   (initialize succeeds)
GET    /github/mcp   status=401   <-- async-notification stream, ungated, no Authorization
POST   /github/mcp   status=401   (session now poisoned; client sends no token)
POST   /github/mcp   status=401
DELETE /github/mcp   status=401

The interceptor's view of that GET — note the absence of authorization and x-forwarded-authorization:

no subject token in forwarded headers; method=GET headerKeys=
  :authority,:method,:path,:scheme,accept,accept-encoding,accept-language,
  mcp-session-id,sec-fetch-mode,user-agent,x-forwarded-proto,x-request-id

And the client side, 5.6s later:

[warning] Authentication session for localhost:8080 removed, stopping server
[info]    Connection state: Stopped

For comparison, a proxy that forwards the upstream's own response for that GET (GitHub replies 405, as it has no standalone stream) works correctly — VS Code logs the following and proceeds normally to tools/list:

[debug] 405 status connecting to <mcp-endpoint> for async notifications;
        they will be disabled: Method Not Allowed

Expected: GET and DELETE on an MCP resource are covered by mcp-auth's methods security configuration like any other MCP request, so that (a) they are authenticated consistently and (b) downstream policies see the same authenticated context they see for a POST.

Workaround: make the backend-auth policy never answer 401 on these paths — return 405 for the notification GET (matching what a real MCP backend without a standalone stream returns) and 204 for the session DELETE (treating a session that cannot be authenticated as already terminated).


Defect 2 — Protected Resource Metadata publishes scopes_supported: [] instead of omitting it

With mcp-auth's requiredScopes empty or unset, the PRM document still contains the key, as an empty array:

{
  "resource": "http://localhost:8290/github/mcp",
  "authorization_servers": ["http://localhost:8080/realms/mcp"],
  "scopes_supported": []
}

scopes_supported is OPTIONAL in RFC 9728 §2, and an empty array is not equivalent to absence. VS Code stores the advertised value as the session's scope set, later recomputes it as undefined, detects a change, and tries to re-acquire a token non-interactively — which fails and takes the session with it:

[info]    Scopes changed from [] to undefined, updating
[warning] Error getting token from server metadata: Error: User did not consent to login.
[warning] Authentication session for localhost:8080 removed, stopping server

Note consentRequired is false on the Keycloak client, and Keycloak logs no consent-related event — "User did not consent to login" is the client's wording for "an interactive login was required but the request was made silently", triggered by the scope-set change.

A proxy that omits the field entirely does not exhibit this: the client sees undefined on both sides, detects no change, and never re-authenticates.

Expected: omit scopes_supported from the PRM when there are no scopes to advertise, rather than emitting [].

Workaround: always configure a non-empty requiredScopes (e.g. openid, profile, email), even when scope enforcement is not wanted — noting that mcp-auth does not enforce requiredScopes anyway, so this is purely to avoid publishing [].

Steps to Reproduce

  1. Deploy an MCP proxy that fronts a remote MCP server, with mcp-auth and a fail-closed backend-auth policy:
apiVersion: gateway.api-platform.wso2.com/v1
kind: Mcp
metadata:
  name: github-mcp-v1.0
spec:
  displayName: GitHub MCP
  version: v1.0
  context: /github
  specVersion: "2025-06-18"
  upstream:
    url: https://api.githubcopilot.com/mcp/
  policies:
    - name: mcp-auth
      version: v1
      params:
        issuers:
          - keycloak-mcp        # any OAuth2 key manager
        requiredScopes: []      # reproduces defect 2
    - name: interceptor-service # any policy that fails closed without an identity
      version: v1
      params:
        endpoint: http://my-interceptor:9100
        request:
          includeRequestHeaders: true
          passthroughOnError: false
  tools: []
  resources: []
  prompts: []
  1. Defect 1 — confirm GET/DELETE are not gated. Both reach the backend chain rather than being rejected by mcp-auth, and carry no authenticated context:
curl -i -X GET    http://localhost:8290/github/mcp -H 'Accept: text/event-stream' -H 'Mcp-Session-Id: test'
curl -i -X DELETE http://localhost:8290/github/mcp -H 'Mcp-Session-Id: test'
# For comparison, POST *is* gated by mcp-auth:
curl -i -X POST   http://localhost:8290/github/mcp -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
  1. Defect 2 — confirm the PRM contains "scopes_supported": []:
curl -s http://localhost:8290/github/.well-known/oauth-protected-resource | jq .
  1. Connect VS Code (1.128.1) to http://localhost:8290/github/mcp and complete the OAuth login. Observe in the MCP output channel (log level Trace): initialize returns a result, notifications/initialized / prompts/list / tools/list are emitted but never reach the gateway, and a few seconds later Authentication session for <auth-server> removed, stopping server. The client then reconnects and loops.

  2. Apply either workaround above and the same client completes initializetools/list normally.

Severity Level of the Issue

Severity/Major (Important functionality is broken. Should be prioritized. Doesn't need immediate attention)

Environment Details

  • Gateway: 1.2.0-beta (mcp-auth v1.1.1) and a locally built 1.2.0-beta-SNAPSHOT (mcp-auth v1.1.2, jwt-auth v1.2.3) — both reproduce
  • MCP client: VS Code 1.128.1 (Claude Code v2.1.x reproduces defect 2, and is unaffected by defect 1 as it sends no session DELETE)
  • Authorization server: Keycloak 26.2
  • Upstream MCP server: GitHub remote MCP (https://api.githubcopilot.com/mcp/)
  • Host: macOS, Docker Desktop

Related

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions