Merge pull request #556 from koic/validate_the_issuer_of_legacy_authorization_server_metadata

Validate the issuer of legacy authorization server metadata
This commit is contained in:
Koichi ITO 2026-09-19 00:11:34 +09:00 • committed by GitHub
commit 9455c41b14
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -405,16 +405,26 @@ module MCP
# Fetches and validates the authorization server's RFC 8414 metadata.
#
# On the modern path the metadata `issuer` must be byte-identical to the discovery URL (RFC 8414 Section 3.3).
# On the legacy 2025-03-26 path that validation is skipped: the legacy spec predates the requirement,
# and a pre-PRM server may host its OAuth endpoints under a path prefix whose `issuer` legitimately differs from
# the origin the metadata was discovered at (neither the TypeScript nor the Python SDK validates the issuer on this path).
# The metadata `issuer` must be byte-identical to the discovery URL (RFC 8414 Section 3.3) on both paths.
# On the legacy 2025-03-26 path the discovery URL is the MCP server's origin, which that spec names as
# the authorization base URL and which a document may render with a trailing slash; the TypeScript and Python SDKs
# accept the same slash-only difference. A document naming any other issuer is refused: an unverified `issuer`
# would otherwise become the identity tokens and client information are bound to, assertions are minted for,
# and the validator is shown, so a server could claim another authorization server and unlock the credentials
# bound to it.
# When even the metadata document is absent, the legacy spec's default endpoints are used.
def authorization_server_metadata(authorization_server:, legacy:, server_url:)
metadata = if legacy
begin
fetched = begin
fetch_authorization_server_metadata(issuer_url: authorization_server)
rescue AuthorizationError
nil
end
if fetched
ensure_legacy_issuer_matches!(expected: authorization_server, returned: fetched["issuer"])
fetched
else
default_legacy_metadata(authorization_server)
end
else
@ -459,6 +469,14 @@ module MCP
fetch_metadata_json(urls, label: "authorization server metadata")
end
# The legacy authorization base is an origin, which a document may render as `https://host/`;
# both name the same server, and nothing else does.
def ensure_legacy_issuer_matches!(expected:, returned:)
return if returned == "#{expected}/"
ensure_issuer_matches!(expected: expected, returned: returned)
end
# Reads `authorization_servers` from a PRM document and returns
# the first entry, raising `AuthorizationError` for any of the malformed
# shapes a non-compliant server could emit (missing field, non-Array