use reqwest::Url;
use serde::Deserialize;
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct ProtectedResourceMetadata {
#[serde(default)]
pub(crate) resource: String,
#[serde(default)]
pub(crate) authorization_servers: Vec<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub(crate) struct AuthServerMetadata {
pub(crate) issuer: String,
pub(crate) authorization_endpoint: String,
pub(crate) token_endpoint: String,
#[serde(default)]
pub(crate) registration_endpoint: Option<String>,
#[serde(default)]
pub(crate) scopes_supported: Vec<String>,
}
pub(crate) fn resource_metadata_url(www_authenticate: Option<&str>) -> Option<String> {
let header = www_authenticate?;
let (_, rest) = header.split_once("resource_metadata=")?;
let rest = rest.strip_prefix('"').unwrap_or(rest);
let url = rest.split_once('"').map_or(rest, |(value, _)| value).trim();
if url.is_empty() {
None
} else {
Some(url.to_string())
}
}
pub(crate) fn well_known_resource_url(mcp_url: &Url) -> Url {
let mut url = mcp_url.clone();
let suffix = match mcp_url.path() {
"/" | "" => String::new(),
path => path.to_string(),
};
url.set_query(None);
url.set_fragment(None);
url.set_path(&format!("/.well-known/oauth-protected-resource{suffix}"));
url
}
pub(crate) fn is_safe_discovery_url(url: &Url) -> bool {
match url.scheme() {
"https" => true,
"http" => matches!(
url.host(),
Some(url::Host::Domain("localhost"))
| Some(url::Host::Ipv4(std::net::Ipv4Addr::LOCALHOST))
| Some(url::Host::Ipv6(std::net::Ipv6Addr::LOCALHOST))
),
_ => false,
}
}
pub(crate) fn same_origin(a: &Url, b: &Url) -> bool {
a.scheme() == b.scheme()
&& a.host() == b.host()
&& a.port_or_known_default() == b.port_or_known_default()
}
pub(crate) fn auth_server_metadata_urls(base: &Url) -> Vec<Url> {
let rfc8414 = base
.join("/.well-known/oauth-authorization-server")
.expect("RFC 8414 path is always joinable");
let openid = base
.join("/.well-known/openid-configuration")
.expect("OpenID configuration path is always joinable");
vec![rfc8414, openid]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn extracts_a_quoted_resource_metadata_url() {
let header = r#"Bearer resource_metadata="https://s.example.com/.well-known/x", error="x""#;
assert_eq!(
resource_metadata_url(Some(header)).as_deref(),
Some("https://s.example.com/.well-known/x")
);
}
#[test]
fn extracts_an_unquoted_resource_metadata_url() {
let header = "Bearer resource_metadata=https://s.example.com/meta";
assert_eq!(
resource_metadata_url(Some(header)).as_deref(),
Some("https://s.example.com/meta")
);
}
#[test]
fn absent_header_yields_none() {
assert_eq!(resource_metadata_url(None), None);
}
#[test]
fn header_without_the_parameter_yields_none() {
assert_eq!(
resource_metadata_url(Some("Bearer error=\"invalid\"")),
None
);
}
#[test]
fn empty_parameter_value_yields_none() {
assert_eq!(
resource_metadata_url(Some(r#"Bearer resource_metadata="""#)),
None
);
}
#[test]
fn well_known_resource_keeps_the_mcp_path() {
let url = Url::parse("https://mcp.example.com/some/mcp").unwrap();
assert_eq!(
well_known_resource_url(&url).as_str(),
"https://mcp.example.com/.well-known/oauth-protected-resource/some/mcp"
);
}
#[test]
fn well_known_resource_preserves_a_trailing_slash() {
let url = Url::parse("https://api.githubcopilot.com/mcp/").unwrap();
assert_eq!(
well_known_resource_url(&url).as_str(),
"https://api.githubcopilot.com/.well-known/oauth-protected-resource/mcp/"
);
}
#[test]
fn well_known_resource_at_the_root_has_no_suffix() {
for root in ["https://mcp.example.com", "https://mcp.example.com/"] {
assert_eq!(
well_known_resource_url(&u(root)).as_str(),
"https://mcp.example.com/.well-known/oauth-protected-resource",
"{root}"
);
}
}
#[test]
fn well_known_resource_drops_query_and_fragment() {
let url = Url::parse("https://mcp.example.com/mcp?tenant=a#frag").unwrap();
assert_eq!(
well_known_resource_url(&url).as_str(),
"https://mcp.example.com/.well-known/oauth-protected-resource/mcp"
);
}
#[test]
fn well_known_resource_keeps_a_nonstandard_port() {
let url = Url::parse("https://mcp.example.com:8443/mcp").unwrap();
assert_eq!(
well_known_resource_url(&url).as_str(),
"https://mcp.example.com:8443/.well-known/oauth-protected-resource/mcp"
);
}
#[test]
fn auth_server_urls_offer_rfc8414_then_openid() {
let urls = auth_server_metadata_urls(&u("https://auth.example.com"));
assert_eq!(
urls[0].as_str(),
"https://auth.example.com/.well-known/oauth-authorization-server"
);
assert_eq!(
urls[1].as_str(),
"https://auth.example.com/.well-known/openid-configuration"
);
}
#[test]
fn protected_resource_metadata_parses() {
let json = r#"{
"resource": "https://mcp.example.com/mcp",
"authorization_servers": ["https://auth.example.com"],
"scopes_supported": ["openid"]
}"#;
let meta: ProtectedResourceMetadata = serde_json::from_str(json).unwrap();
assert_eq!(meta.resource, "https://mcp.example.com/mcp");
assert_eq!(meta.authorization_servers, vec!["https://auth.example.com"]);
}
#[test]
fn auth_server_metadata_parses_with_optional_registration() {
let json = r#"{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token",
"registration_endpoint": "https://auth.example.com/register",
"scopes_supported": ["openid", "profile"]
}"#;
let meta: AuthServerMetadata = serde_json::from_str(json).unwrap();
assert_eq!(meta.issuer, "https://auth.example.com");
assert_eq!(
meta.registration_endpoint.as_deref(),
Some("https://auth.example.com/register")
);
assert_eq!(meta.scopes_supported.len(), 2);
}
#[test]
fn auth_server_metadata_registration_is_optional() {
let json = r#"{
"issuer": "https://auth.example.com",
"authorization_endpoint": "https://auth.example.com/authorize",
"token_endpoint": "https://auth.example.com/token"
}"#;
let meta: AuthServerMetadata = serde_json::from_str(json).unwrap();
assert!(meta.registration_endpoint.is_none());
assert!(meta.scopes_supported.is_empty());
}
fn u(s: &str) -> Url {
Url::parse(s).expect("test URL parses")
}
#[test]
fn discovery_requires_https_off_loopback() {
assert!(is_safe_discovery_url(&u("https://auth.example.com/x")));
assert!(!is_safe_discovery_url(&u("http://auth.example.com/x")));
assert!(!is_safe_discovery_url(&u("ftp://auth.example.com/x")));
}
#[test]
fn discovery_permits_http_on_loopback() {
for url in [
"http://localhost:8080/x",
"http://127.0.0.1:8080/x",
"http://[::1]:8080/x",
] {
assert!(is_safe_discovery_url(&u(url)), "{url}");
}
assert!(!is_safe_discovery_url(&u("http://localhost.evil.com/x")));
}
#[test]
fn same_origin_compares_scheme_host_and_port() {
let mcp = u("https://mcp.example.com/mcp");
assert!(same_origin(
&u("https://mcp.example.com/.well-known/x"),
&mcp
));
assert!(same_origin(&u("https://mcp.example.com:443/x"), &mcp));
assert!(!same_origin(&u("https://evil.example.com/x"), &mcp));
assert!(!same_origin(&u("http://mcp.example.com/x"), &mcp));
assert!(!same_origin(&u("https://mcp.example.com:8443/x"), &mcp));
assert!(!same_origin(&u("https://a.mcp.example.com/x"), &mcp));
assert!(!same_origin(&u("https://mcp.example.com.evil.com/x"), &mcp));
assert!(!same_origin(&u("http://169.254.169.254/latest/"), &mcp));
}
}