rws_server/sso/saml/
metadata.rs1use crate::rsa_key::RsaPublicKey;
4
5use super::der::extract_subject_public_key_info;
6use super::xml;
7use crate::sso::pkce::base64url_decode;
8use crate::sso::SsoError;
9
10pub struct SamlIdpMetadata {
13 pub entity_id: String,
15 pub sso_url: String,
21 pub signing_key: RsaPublicKey,
25}
26
27const POST_BINDING: &str = "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST";
28
29impl SamlIdpMetadata {
30 pub fn from_str(xml_str: &str) -> Result<Self, SsoError> {
32 let root = xml::parse(xml_str).map_err(SsoError)?;
33 let entity_descriptor = if root.name == "EntityDescriptor" { &root } else { root.find("EntityDescriptor").ok_or_else(|| SsoError("metadata is missing EntityDescriptor".into()))? };
34 let entity_id = entity_descriptor
35 .attr("entityID")
36 .ok_or_else(|| SsoError("EntityDescriptor is missing entityID".into()))?
37 .to_string();
38
39 let idp_descriptor = entity_descriptor
40 .find("IDPSSODescriptor")
41 .ok_or_else(|| SsoError("metadata is missing IDPSSODescriptor".into()))?;
42
43 let mut sso_services = Vec::new();
44 idp_descriptor.find_all("SingleSignOnService", &mut sso_services);
45 if sso_services.is_empty() {
46 return Err(SsoError("metadata has no SingleSignOnService".into()));
47 }
48 let sso_url = sso_services
49 .iter()
50 .find(|s| s.attr("Binding") == Some(POST_BINDING))
51 .or_else(|| sso_services.first())
52 .and_then(|s| s.attr("Location"))
53 .ok_or_else(|| SsoError("SingleSignOnService is missing Location".into()))?
54 .to_string();
55
56 let mut key_descriptors = Vec::new();
57 idp_descriptor.find_all("KeyDescriptor", &mut key_descriptors);
58 let signing_descriptor = key_descriptors
59 .iter()
60 .find(|k| k.attr("use") == Some("signing"))
61 .or_else(|| key_descriptors.first())
62 .ok_or_else(|| SsoError("metadata has no KeyDescriptor with a signing certificate".into()))?;
63 let cert_b64 = signing_descriptor
64 .find("X509Certificate")
65 .map(|n| n.text.clone())
66 .ok_or_else(|| SsoError("KeyDescriptor is missing X509Certificate".into()))?;
67 let cert_der = decode_base64_flexible(&cert_b64)?;
68 let spki_der = extract_subject_public_key_info(&cert_der).map_err(SsoError)?;
69 let signing_key = RsaPublicKey::from_public_key_der(&spki_der)
70 .map_err(|e| SsoError(format!("signing certificate does not contain an RSA public key: {e}")))?;
71
72 Ok(SamlIdpMetadata { entity_id, sso_url, signing_key })
73 }
74
75 pub fn from_file(path: &str) -> Result<Self, SsoError> {
77 let contents = std::fs::read_to_string(path).map_err(|e| SsoError(format!("failed to read {path}: {e}")))?;
78 Self::from_str(&contents)
79 }
80
81 pub fn from_url(url: &str) -> Result<Self, SsoError> {
83 let resp = crate::http_client::Client::new()
84 .get(url)
85 .timeout_ms(10_000)
86 .send()
87 .map_err(|e| SsoError(format!("metadata fetch failed: {e}")))?;
88 if !resp.is_success() {
89 return Err(SsoError(format!("metadata endpoint returned {}", resp.status())));
90 }
91 let body = resp.text().map_err(|e| SsoError(e.to_string()))?;
92 Self::from_str(&body)
93 }
94}
95
96#[doc(hidden)]
102pub fn decode_base64_flexible(s: &str) -> Result<Vec<u8>, SsoError> {
103 let cleaned: String = s.chars().filter(|c| !c.is_whitespace()).collect();
104 base64url_decode(&cleaned).map_err(SsoError)
105}