use crate::util::log::info;
use crate::xml::xml_escape;
use super::config::SamlConfig;
#[must_use]
pub fn generate_sp_metadata(config: &SamlConfig) -> String {
let entity_id = xml_escape(config.entity_id());
let acs_url = xml_escape(config.acs_url());
info!(entity_id = %config.entity_id(), "saml: SP metadata generated");
format!(
r#"<?xml version="1.0" encoding="UTF-8"?>
<md:EntityDescriptor xmlns:md="urn:oasis:names:tc:SAML:2.0:metadata"
entityID="{entity_id}">
<md:SPSSODescriptor AuthnRequestsSigned="false"
WantAssertionsSigned="true"
protocolSupportEnumeration="urn:oasis:names:tc:SAML:2.0:protocol">
<md:AssertionConsumerService Binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"
Location="{acs_url}"
index="0"
isDefault="true"/>
</md:SPSSODescriptor>
</md:EntityDescriptor>"#
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::xml::parse_xml;
fn test_config() -> SamlConfig {
SamlConfig::new(
"https://sp.example.com",
"https://sp.example.com/acs",
"https://idp.example.com/sso",
"https://idp.example.com",
)
.unwrap()
}
#[test]
fn metadata_is_valid_xml() {
let xml = generate_sp_metadata(&test_config());
let root = parse_xml(&xml).unwrap();
assert_eq!(root.name(), "EntityDescriptor");
}
#[test]
fn metadata_contains_entity_id() {
let xml = generate_sp_metadata(&test_config());
let root = parse_xml(&xml).unwrap();
assert_eq!(root.attribute("entityID"), Some("https://sp.example.com"));
}
#[test]
fn metadata_contains_sp_descriptor() {
let xml = generate_sp_metadata(&test_config());
let root = parse_xml(&xml).unwrap();
let sp = root.find_child("SPSSODescriptor").unwrap();
assert_eq!(sp.attribute("WantAssertionsSigned"), Some("true"));
assert_eq!(
sp.attribute("protocolSupportEnumeration"),
Some("urn:oasis:names:tc:SAML:2.0:protocol")
);
}
#[test]
fn metadata_contains_acs() {
let xml = generate_sp_metadata(&test_config());
let root = parse_xml(&xml).unwrap();
let sp = root.find_child("SPSSODescriptor").unwrap();
let acs = sp.find_child("AssertionConsumerService").unwrap();
assert_eq!(
acs.attribute("Location"),
Some("https://sp.example.com/acs")
);
assert_eq!(
acs.attribute("Binding"),
Some("urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST")
);
assert_eq!(acs.attribute("index"), Some("0"));
assert_eq!(acs.attribute("isDefault"), Some("true"));
}
#[test]
fn metadata_escapes_special_chars() {
let config = SamlConfig::new(
"https://sp.example.com?a=1&b=2",
"https://sp.example.com/acs?x=1&y=2",
"https://idp.example.com/sso",
"https://idp.example.com",
)
.unwrap();
let xml = generate_sp_metadata(&config);
assert!(xml.contains("&"));
let root = parse_xml(&xml).unwrap();
assert_eq!(
root.attribute("entityID"),
Some("https://sp.example.com?a=1&b=2")
);
}
}