Skip to content

feat: require OAuth/OIDC bearer auth on HTTP transport (OWASP MCP07)#1209

Open
manjunathshiva wants to merge 1 commit into
mongodb-js:mainfrom
manjunathshiva:feat/mcp07-oauth-http-transport
Open

feat: require OAuth/OIDC bearer auth on HTTP transport (OWASP MCP07)#1209
manjunathshiva wants to merge 1 commit into
mongodb-js:mainfrom
manjunathshiva:feat/mcp07-oauth-http-transport

Conversation

@manjunathshiva
Copy link
Copy Markdown

Per OWASP MCP Top 10 (2025) item MCP07 - Insufficient Authentication & Authorization. The HTTP transport now refuses to serve any request without a valid OAuth/OIDC bearer token when oauthIssuer is configured, and refuses to start at all when bound to a non-loopback host without authentication.

This closes the "open HTTP backdoor" gap: previously, running mongodb-mcp-server --transport http --httpHost 0.0.0.0 exposed every configured MongoDB tool to anyone who could reach the port. Now such deployments must (a) bind only to loopback, or (b) configure an OIDC issuer whose bearer tokens are validated on every request. Loopback dev workflows are unaffected.

Scope (intentional v1):

  • AuthN only. Per-tool scope mapping (e.g. mcp:read scope required for read tools) is conceptually separate and tracked as a follow-up; the middleware exposes the parsed scope/scp claim on the auth context so a v2 can add scope checks without restructuring.
  • Bearer-token validation only. RFC 7662 introspection (which would give revoke-before-expiry) is not implemented; we trust signature + exp + iss + aud.

New module: src/transports/auth/jwksCache.ts
LRUCache-backed wrapper around oauth4webapi's discovery + JWKS fetch. Caches AuthorizationServer metadata per issuer for a configurable TTL (default 10 minutes); failures are NOT cached so transient issuer unavailability self-heals.

oauthMiddleware.ts
Express middleware. Validates Authorization: Bearer <jwt> via oauth4webapi.validateJwtAccessToken (RFC 9068). Verifies signature against the issuer's JWKS, checks iss/aud/exp/nbf. On failure returns 401 with a spec-compliant WWW-Authenticate: Bearer realm=... error=... header. On success sets a typed AuthContext ({ sub, scopes, audience, issuer }) on the request, accessible via the exported getAuthContext() helper. Accepts both RFC 8693 scope (space-delimited string) and Azure-style scp (array). The raw token is never stored past the middleware to keep it out of logs/telemetry.

index.ts
Public surface: JwksCache, createOAuthMiddleware, getAuthContext, AuthContext, OAuthMiddlewareOptions. Re-exported from both mongodb-mcp-server (lib.ts) and mongodb-mcp-server/web (web.ts) so embedders building custom HTTP servers can layer auth on top.

New config fields (userConfig.ts):

  • oauthIssuer OIDC issuer URL. Validated as a URL at parse time.
  • oauthAudience Expected aud claim. Required when oauthIssuer
    is set; mismatched pairs fail at startup with a
    clear error.
  • oauthJwksCacheTtlMs How long to cache the issuer's metadata.
    Default 600_000 (10 minutes); minimum 1_000.

All three are overrideBehavior: "not-allowed" so per-session overrides cannot disable auth.

Wiring:

  • MCPHttpServer.setupMiddlewares mounts createOAuthMiddleware before any MCP-routing middleware when oauthIssuer + oauthAudience are set, so unauthenticated requests cannot reach session state. Mounts the shared JwksCache so all requests share one cache.
  • StreamableHttpRunner.validateConfig refuses to start in two cases: (a) httpHost is non-loopback AND oauthIssuer/oauthAudience are not set together -> "Refusing to start: ... non-loopback ... OAuth authentication is not configured" (b) exactly one of oauthIssuer / oauthAudience is set -> "must be configured together". Misconfigured pairs are almost always a mistake; fail loudly.

LogIds (1_006_200 - 1_006_205):

  • httpOAuthDisabled / httpOAuthEnabled (startup audit)
  • httpOAuthMissingToken (debug; 401)
  • httpOAuthInvalidToken (warning; 401 with token error)
  • httpOAuthJwksFetchFailure (error)
  • httpOAuthDiscoveryFailure (error)

Tests:

  • 7 unit tests for createOAuthMiddleware: missing/non-Bearer header -> 401 invalid_request, issuer unreachable -> 503, oauth4webapi rejection -> 401 invalid_token, RFC 8693 / Azure / missing scope claim shapes, audience normalisation.
  • 6 unit tests for JwksCache: cache hit, cache miss + fetch, failure non-caching, error logging paths, clear().
  • 4 unit tests for StreamableHttpRunner.validateConfig: refuses on non-loopback without OAuth, refuses on half-configured OAuth pair (issuer-only and audience-only), accepts loopback without OAuth.
  • tests/unit/common/config.test.ts expectedDefaults includes the new oauthJwksCacheTtlMs default.

api-extractor reports regenerated.

BREAKING CHANGE: HTTP transport deployments that bind to a non-loopback interface (httpHost != "127.0.0.1"/"localhost"/"::1") now require OAuth configuration (oauthIssuer + oauthAudience) and the server refuses to start without it. Deployments bound to loopback are unaffected.

Proposed changes

Checklist

Per OWASP MCP Top 10 (2025) item MCP07 - Insufficient Authentication
& Authorization. The HTTP transport now refuses to serve any request
without a valid OAuth/OIDC bearer token when `oauthIssuer` is
configured, and refuses to start at all when bound to a non-loopback
host without authentication.

This closes the "open HTTP backdoor" gap: previously, running
`mongodb-mcp-server --transport http --httpHost 0.0.0.0` exposed
every configured MongoDB tool to anyone who could reach the port.
Now such deployments must (a) bind only to loopback, or (b)
configure an OIDC issuer whose bearer tokens are validated on every
request. Loopback dev workflows are unaffected.

Scope (intentional v1):
- AuthN only. Per-tool scope mapping (e.g. mcp:read scope required
  for read tools) is conceptually separate and tracked as a
  follow-up; the middleware exposes the parsed `scope`/`scp` claim
  on the auth context so a v2 can add scope checks without
  restructuring.
- Bearer-token validation only. RFC 7662 introspection (which would
  give revoke-before-expiry) is not implemented; we trust signature
  + exp + iss + aud.

New module: src/transports/auth/
  jwksCache.ts
    LRUCache-backed wrapper around oauth4webapi's discovery + JWKS
    fetch. Caches AuthorizationServer metadata per issuer for a
    configurable TTL (default 10 minutes); failures are NOT cached
    so transient issuer unavailability self-heals.

  oauthMiddleware.ts
    Express middleware. Validates `Authorization: Bearer <jwt>` via
    oauth4webapi.validateJwtAccessToken (RFC 9068). Verifies
    signature against the issuer's JWKS, checks
    `iss`/`aud`/`exp`/`nbf`. On failure returns 401 with a
    spec-compliant `WWW-Authenticate: Bearer realm=... error=...`
    header. On success sets a typed AuthContext
    ({ sub, scopes, audience, issuer }) on the request, accessible
    via the exported getAuthContext() helper. Accepts both RFC 8693
    `scope` (space-delimited string) and Azure-style `scp` (array).
    The raw token is never stored past the middleware to keep it out
    of logs/telemetry.

  index.ts
    Public surface: JwksCache, createOAuthMiddleware,
    getAuthContext, AuthContext, OAuthMiddlewareOptions. Re-exported
    from both `mongodb-mcp-server` (lib.ts) and
    `mongodb-mcp-server/web` (web.ts) so embedders building custom
    HTTP servers can layer auth on top.

New config fields (userConfig.ts):
- oauthIssuer       OIDC issuer URL. Validated as a URL at parse time.
- oauthAudience     Expected `aud` claim. Required when oauthIssuer
                    is set; mismatched pairs fail at startup with a
                    clear error.
- oauthJwksCacheTtlMs   How long to cache the issuer's metadata.
                    Default 600_000 (10 minutes); minimum 1_000.

All three are `overrideBehavior: "not-allowed"` so per-session
overrides cannot disable auth.

Wiring:
- MCPHttpServer.setupMiddlewares mounts createOAuthMiddleware before
  any MCP-routing middleware when oauthIssuer + oauthAudience are
  set, so unauthenticated requests cannot reach session state.
  Mounts the shared JwksCache so all requests share one cache.
- StreamableHttpRunner.validateConfig refuses to start in two cases:
  (a) httpHost is non-loopback AND oauthIssuer/oauthAudience are not
      set together  -> "Refusing to start: ... non-loopback ...
      OAuth authentication is not configured"
  (b) exactly one of oauthIssuer / oauthAudience is set  -> "must be
      configured together". Misconfigured pairs are almost always a
      mistake; fail loudly.

LogIds (1_006_200 - 1_006_205):
- httpOAuthDisabled / httpOAuthEnabled  (startup audit)
- httpOAuthMissingToken                 (debug; 401)
- httpOAuthInvalidToken                 (warning; 401 with token error)
- httpOAuthJwksFetchFailure             (error)
- httpOAuthDiscoveryFailure             (error)

Tests:
- 7 unit tests for createOAuthMiddleware: missing/non-Bearer header
  -> 401 invalid_request, issuer unreachable -> 503, oauth4webapi
  rejection -> 401 invalid_token, RFC 8693 / Azure / missing scope
  claim shapes, audience normalisation.
- 6 unit tests for JwksCache: cache hit, cache miss + fetch, failure
  non-caching, error logging paths, clear().
- 4 unit tests for StreamableHttpRunner.validateConfig: refuses on
  non-loopback without OAuth, refuses on half-configured OAuth pair
  (issuer-only and audience-only), accepts loopback without OAuth.
- tests/unit/common/config.test.ts expectedDefaults includes the
  new oauthJwksCacheTtlMs default.

api-extractor reports regenerated.

BREAKING CHANGE: HTTP transport deployments that bind to a
non-loopback interface (httpHost != "127.0.0.1"/"localhost"/"::1")
now require OAuth configuration (oauthIssuer + oauthAudience) and
the server refuses to start without it. Deployments bound to
loopback are unaffected.
@manjunathshiva manjunathshiva requested a review from a team as a code owner May 27, 2026 09:54
@manjunathshiva manjunathshiva requested review from jeroenvervaeke and removed request for a team May 27, 2026 09:54
Copy link
Copy Markdown
Collaborator

@nirinchev nirinchev left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We appreciate the PRs you're opening and your desire to improve the security posture of the MongoDB MCP server, but those are significant changes that should be coordinated with the team prior to opening a PR. It is an explicit non-goal for the http transport server to be prescriptive or self-sufficient when it comes to authentication. Instead, the expectation is that users deploy it behind a reverse proxy with auth configured that matches their business needs. We do have a section in the docs that covers this: https://www.mongodb.com/docs/mcp-server/security-best-practices/#remote-mcp-server. While we recognize docs do not solve everything, different customers have different authN/Z needs, which is why we have opted against bundling OAuth into the default MCP server.

I'll bring it up in our team sync as a discussion point and we may decide to change our stance, but until then, we cannot merge this.

@manjunathshiva
Copy link
Copy Markdown
Author

We want to use Mongo DB MCP Server in Production for Agentic AI Project. We cannot take this as it is not OWSAP MCP10 compliant. We made changes to fix the security issues and also few more like removing delete operations , only allow certificates. All changes are in https://github.com/manjunathshiva/mongodb-mcp-server/tree/owsap_to10_fixes_manjunath branch and it is working fine. Thought of giving back OWSAP fixes to community. Hence raised 3 PR's to fix all OWSAP MCP 10 issues.

@nirinchev
Copy link
Copy Markdown
Collaborator

Understood and appreciate contributing the fixes - main problem is that since this is a product used in many different environments, we need to ensure the changes will not be disruptive to other customers. For example, someone who has an external system managing AuthN/Z would probably need a way to disable the built-in one.

The MongoDB MCP server is intended to be pluggable and extensible so customer who have more specific needs can use it as a library and inject their business/auth logic. So rather than changing the core library, the recommended approach would be to use the extension points to add your validations (this should work for OAuth, but may not work that well for some of the other PRs you've raised, where a different approach may be needed).

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.

2 participants