Merge pull request #431 from koic/iss_parameter_validation

Validate the RFC 9207 `iss` Authorization Response Parameter per SEP-2468
This commit is contained in:
Koichi ITO 2026-07-12 02:11:52 +09:00 • committed by GitHub
commit 8aae052efd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 163 additions and 5 deletions

View file

@ -74,13 +74,20 @@ module MCP
)
@provider.redirect_handler.call(authorization_url)
code, returned_state = Array(@provider.callback_handler.call)
callback_result = Array(@provider.callback_handler.call)
code, returned_state, returned_iss = callback_result
raise AuthorizationError, "Authorization callback did not return an authorization code." unless code
unless states_match?(returned_state, state)
raise AuthorizationError, "OAuth state mismatch (CSRF protection)."
end
validate_authorization_response_issuer!(
as_metadata: as_metadata,
iss: returned_iss,
iss_provided: callback_result.length >= 3,
)
tokens = exchange_authorization_code(
as_metadata: as_metadata,
client_info: client_info,
@ -534,6 +541,43 @@ module MCP
raise AuthorizationError, "#{label} #{url.inspect} is not a valid URI: #{e.message}."
end
# Validates the RFC 9207 `iss` authorization response parameter against the issuer of the authorization server
# the flow is talking to, per SEP-2468 (mix-up attack mitigation in multi-IdP setups):
#
# - When the callback supplied a non-empty `iss`, it MUST equal the AS metadata `issuer` exactly (simple string comparison,
# no normalization, per RFC 9207 Section 2.4); on mismatch the flow aborts before the authorization code is ever sent to
# a token endpoint.
# - When the callback returned a 3-element `[code, state, iss]` whose `iss` is nil (asserting it inspected the authorization response
# and found no `iss`) and the AS metadata advertises `authorization_response_iss_parameter_supported: true`, the missing parameter
# is treated as an attack per RFC 9207 Section 2.4 and the flow aborts.
# - A legacy 2-element `[code, state]` callback skips the check: the caller never looked for `iss`, so its absence carries no signal.
#
# `as_metadata["issuer"]` has already been byte-compared against the discovery URL by `ensure_issuer_matches!`,
# so it is the RFC 9207 anchor value. `iss` is not a secret; a plain `==` suffices.
#
# - https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2468
# - https://www.rfc-editor.org/rfc/rfc9207
def validate_authorization_response_issuer!(as_metadata:, iss:, iss_provided:)
expected = as_metadata["issuer"]
if iss && !iss.to_s.empty?
return if iss.to_s == expected.to_s
raise AuthorizationError,
"Authorization response `iss` does not match the authorization server issuer " \
"(expected #{expected.inspect}, got #{iss.inspect}) (RFC 9207); " \
"refusing to exchange the authorization code."
end
return unless iss_provided
return unless as_metadata["authorization_response_iss_parameter_supported"] == true
raise AuthorizationError,
"Authorization server advertises `authorization_response_iss_parameter_supported` but " \
"the authorization response carried no `iss` parameter (RFC 9207 Section 2.4); " \
"refusing to exchange the authorization code."
end
# Constant-time comparison for the OAuth `state` parameter to prevent timing-based discovery
# of the expected value.
# `OpenSSL.fixed_length_secure_compare` would be ideal, but it is not available on Ruby 2.7

View file

@ -21,8 +21,13 @@ module MCP
# - `redirect_handler` - Callable invoked with the fully-built authorization
# URL (a `URI`). Implementations typically open the user's browser.
# - `callback_handler` - Callable invoked after `redirect_handler`. Returns
# `[code, state]` where `code` is the authorization code and `state` is
# the `state` parameter received on the redirect URI.
# `[code, state]` or `[code, state, iss]`, where `code` is the authorization code,
# `state` is the `state` parameter received on the redirect URI, and `iss` is
# the RFC 9207 `iss` parameter (or nil when the authorization response carried none).
# Returning the 3-element form opts into SEP-2468 issuer validation: a present `iss`
# must match the authorization server's issuer, and a nil `iss` is
# rejected when the AS advertises `authorization_response_iss_parameter_supported`.
# The 2-element form skips the check for backward compatibility.
#
# Optional keyword arguments:
# - `scope` - String of space-separated scopes to request when the server's