Skip to content

RFC 9728 OAuth Protected Resource Metadata Complianceยถ

Overviewยถ

ContextForge implements RFC 9728: OAuth 2.0 Protected Resource Metadata to enable OAuth-protected MCP servers to advertise their authorization server configuration. This allows MCP clients (like Claude Desktop, MCP Inspector) to discover OAuth endpoints and initiate browser-based SSO flows.

RFC 9728 Requirementsยถ

Per RFC 9728 Section 3.1, the well-known URI for OAuth Protected Resource Metadata is constructed by:

  1. Taking the protected resource URL: https://gateway.example.com/servers/{server_id}/mcp
  2. Removing any trailing slash
  3. Inserting /.well-known/oauth-protected-resource/ after the scheme and authority
  4. Result: https://gateway.example.com/.well-known/oauth-protected-resource/servers/{server_id}/mcp

Response Formatยถ

The metadata response fields (per RFC 9728 Section 2):

  • resource (string, required): The protected resource identifier URL
  • authorization_servers (array, optional per RFC 9728, required per MCP spec): JSON array of authorization server issuer URIs
  • bearer_methods_supported (array, optional): Supported bearer token methods (e.g., ["header"])
  • scopes_supported (array, optional): List of supported OAuth scopes

Example Response (per RFC 9728 Section 3.2):

{
  "resource": "https://gateway.example.com/servers/abc-123/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["read", "write"]
}

Implementationยถ

Compliant Endpointยถ

Path: /.well-known/oauth-protected-resource/servers/{server_id}/mcp

Method: GET

Authentication: None required (per RFC 9728)

Implementation: mcpgateway/routers/well_known.py:118

Features:

  • Path-based discovery (not query parameters)
  • UUID validation for server_id (prevents path traversal)
  • Returns authorization_servers field as JSON array (RFC 9728 Section 2)
  • Includes /mcp suffix in resource URL
  • Cache headers for performance
  • Only exposes public servers with OAuth enabled

Example Request:

curl https://gateway.example.com/.well-known/oauth-protected-resource/servers/550e8400-e29b-41d4-a716-446655440000/mcp

Example Response:

{
  "resource": "https://gateway.example.com/servers/550e8400-e29b-41d4-a716-446655440000/mcp",
  "authorization_servers": ["https://auth.example.com"],
  "bearer_methods_supported": ["header"],
  "scopes_supported": ["openid", "profile", "email"]
}

Service Layerยถ

Implementation: mcpgateway/services/server_service.py:1913

The get_oauth_protected_resource_metadata() method:

  • Returns RFC 9728 compliant metadata with authorization_servers field (JSON array)
  • Reads authorization_servers (plural) from config as primary source
  • Falls back to authorization_server (singular) from config for backward compatibility
  • Only exposes metadata for public, enabled servers with OAuth configured
  • Includes optional scopes_supported if configured

Configuration Priority:

  1. oauth_config.authorization_servers (array, RFC 9728 compliant)
  2. oauth_config.authorization_server (string, legacy fallback โ€” wrapped in array)

Securityยถ

UUID Validation:

The endpoint validates that server_id is a valid UUID using regex pattern matching. This prevents:

  • Path traversal attacks (../admin)
  • SQL injection attempts
  • Arbitrary path access

Access Control:

  • Only public servers expose OAuth metadata
  • Disabled servers return 404
  • Private/team servers return 404 (prevents information leakage)
  • OAuth must be explicitly enabled on the server

Implementation: mcpgateway/routers/well_known.py:39

UUID_PATTERN = re.compile(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", re.IGNORECASE)

Per-Server OAuth Enforcementยถ

Overviewยถ

When a virtual server has oauth_enabled=True, ContextForge enforces authentication for that server's MCP endpoints regardless of the global MCP_REQUIRE_AUTH setting. This closes the gap where OAuth capability was advertised via RFC 9728 metadata but never enforced on subsequent MCP requests.

Behaviorยถ

  • Permissive mode (MCP_REQUIRE_AUTH=false): Unauthenticated requests to oauth_enabled servers are rejected with HTTP 401. Non-OAuth servers continue to allow unauthenticated access with public-only visibility.
  • Strict mode (MCP_REQUIRE_AUTH=true): All unauthenticated requests are already rejected by the middleware, so per-server enforcement is not needed.
  • Authenticated requests: Requests with a valid Bearer token are always allowed through, regardless of the server's oauth_enabled flag.

WWW-Authenticate Headerยถ

When an unauthenticated request is rejected, the 401 response includes a WWW-Authenticate header with the RFC 9728 resource metadata URL:

WWW-Authenticate: Bearer resource_metadata="https://gateway.example.com/.well-known/oauth-protected-resource/servers/{server_id}/mcp"

This enables MCP clients to discover the authorization server and initiate an OAuth flow automatically.

Deployment note: The resource_metadata URL is constructed from the request's Host and X-Forwarded-Proto headers. Deployments must ensure their reverse proxy sets these headers authoritatively to prevent challenge URL poisoning by untrusted clients.

Fail-Closed on Infrastructure Errorsยถ

If the database is unavailable when checking a server's oauth_enabled flag, the request is rejected with HTTP 503 (Service Unavailable) rather than silently allowed. This fail-closed behavior prevents unauthenticated access during infrastructure outages.

Defense-in-Depthยถ

OAuth enforcement is applied at two levels in the Streamable HTTP transport:

  1. Middleware (streamable_http_auth): Primary enforcement point โ€” checks oauth_enabled before allowing unauthenticated requests through in permissive mode.
  2. MCP handlers (list_tools, call_tool, list_prompts, get_prompt, list_resources, read_resource, list_resource_templates, set_logging_level, complete): Secondary enforcement โ€” each handler re-checks per-server OAuth as a defense-in-depth guard, with results cached per-request via a ContextVar to avoid redundant DB lookups.

Deprecated Endpointsยถ

Query Parameter Endpoint (Deprecated)ยถ

Path: /.well-known/oauth-protected-resource?server_id={id}

Status: Returns 404 with deprecation message

Reason: Non-compliant with RFC 9728 (uses query parameters instead of path-based discovery)

Migration: Use /.well-known/oauth-protected-resource/servers/{server_id}/mcp

Server-Scoped Endpoint (Deprecated)ยถ

Path: /servers/{server_id}/.well-known/oauth-protected-resource

Status: Returns 301 redirect to compliant endpoint

Reason: Non-compliant with RFC 9728 (appends well-known path instead of inserting it)

Migration: Automatically redirects to /.well-known/oauth-protected-resource/servers/{server_id}/mcp

Configurationยถ

Server OAuth Configurationยถ

To enable OAuth Protected Resource Metadata for a server, configure the server's oauth_config:

{
  "oauth_enabled": true,
  "oauth_config": {
    "authorization_servers": ["https://auth.example.com"],
    "scopes_supported": ["openid", "profile", "email"],
    "client_id": "your-client-id",
    "client_secret": "your-client-secret"
  }
}

Required Fields:

  • authorization_servers (array): JSON array of authorization server issuer URIs (at least one required per MCP spec)

Optional Fields:

  • scopes_supported (array): List of supported OAuth scopes
  • client_id (string): OAuth client ID (not exposed in metadata)
  • client_secret (string): OAuth client secret (never exposed)

Legacy Configuration:

For backward compatibility, the system also accepts a singular string form:

{
  "authorization_server": "https://auth.example.com"
}

This is automatically wrapped in an array in the response.

Global Settingsยถ

Environment Variables:

  • WELL_KNOWN_ENABLED=true - Enable well-known endpoints (default: true)
  • WELL_KNOWN_CACHE_MAX_AGE=3600 - Cache duration in seconds (default: 3600)

Testingยถ

Unit Testsยถ

Comprehensive test suite: tests/unit/mcpgateway/routers/test_well_known_rfc9728.py

Test Coverage:

  • RFC 9728 compliant endpoint success cases
  • Path validation and security (UUID validation, path traversal prevention)
  • Deprecated endpoint behavior (404 and 301 responses)
  • Service layer RFC 9728 compliance
  • Backward compatibility with legacy configurations
  • Security validation (SQL injection, path traversal, access control)

Run Tests:

pytest tests/unit/mcpgateway/routers/test_well_known_rfc9728.py -v

Manual Testingยถ

Test with curl:

# RFC 9728 compliant endpoint
curl -i https://gateway.example.com/.well-known/oauth-protected-resource/servers/550e8400-e29b-41d4-a716-446655440000/mcp

# Verify response includes authorization_servers array
curl -s https://gateway.example.com/.well-known/oauth-protected-resource/servers/550e8400-e29b-41d4-a716-446655440000/mcp | jq .

# Test deprecated query-param endpoint (should return 404)
curl -i https://gateway.example.com/.well-known/oauth-protected-resource?server_id=550e8400-e29b-41d4-a716-446655440000

# Test deprecated server-scoped endpoint (should return 301)
curl -i https://gateway.example.com/servers/550e8400-e29b-41d4-a716-446655440000/.well-known/oauth-protected-resource

Test with MCP Inspector:

  1. Configure an OAuth-enabled server in ContextForge
  2. Open MCP Inspector
  3. Connect to the server using the MCP endpoint: https://gateway.example.com/servers/{server_id}/mcp
  4. MCP Inspector should automatically discover OAuth metadata via RFC 9728
  5. Verify browser-based OAuth flow initiates correctly

Migration Guideยถ

For ContextForge Administratorsยถ

No action required. The implementation maintains backward compatibility:

  • Existing servers with authorization_server (singular string) continue to work
  • The value is automatically wrapped in an array for the RFC 9728 response
  • Deprecated endpoints provide clear migration guidance

Recommended Actions:

  1. Update server configurations to use authorization_servers (plural array) field
  2. Test OAuth flows with MCP clients
  3. Monitor logs for deprecated endpoint usage

For MCP Client Developersยถ

Update OAuth discovery logic:

Before (Non-compliant):

# Query parameter approach (deprecated)
metadata_url = f"{base_url}/.well-known/oauth-protected-resource?server_id={server_id}"

After (RFC 9728 Compliant):

# Path-based approach (RFC 9728)
resource_url = f"{base_url}/servers/{server_id}/mcp"
# Insert /.well-known/oauth-protected-resource/ after scheme and authority
parsed = urlparse(resource_url)
metadata_url = f"{parsed.scheme}://{parsed.netloc}/.well-known/oauth-protected-resource{parsed.path}"

Handle authorization_servers field:

# Parse metadata response
metadata = requests.get(metadata_url).json()

# RFC 9728 Section 2: authorization_servers is a JSON array
auth_servers = metadata["authorization_servers"]  # e.g., ["https://auth.example.com"]

Compliance Checklistยถ

  • Path-based discovery (not query parameters)
  • authorization_servers field as JSON array (RFC 9728 Section 2)
  • Resource URL includes /mcp suffix
  • No authentication required for metadata endpoint
  • Cache headers for performance
  • UUID validation for security
  • Only public servers exposed
  • Backward compatibility with legacy configs
  • Comprehensive test coverage
  • Deprecated endpoints provide migration guidance

Referencesยถ