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)

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ΒΆ