Skip to main content

alibi_plugins/sso/
sp_metadata.rs

1//! Public SAML service-provider discovery for registered providers.
2use super::{
3    AuthContext, AuthError, AuthResult, AuthSchema, EndpointResponse, JsValue, ServerEndpoint,
4    SsoPlugin, Value, api, json,
5};
6use quick_xml::{
7    Writer,
8    events::{BytesEnd, BytesStart, BytesText, Event},
9};
10impl SsoPlugin {
11    pub fn sp_metadata_endpoint(provider_id: &str) -> AuthResult<ServerEndpoint<Value>> {
12        ServerEndpoint::new("sso", "spMetadata").with_query(&json!({"providerId":provider_id}))
13    }
14    pub(super) async fn metadata<S: AuthSchema>(
15        &self,
16        query: &JsValue,
17        ctx: &AuthContext<S>,
18    ) -> AuthResult<EndpointResponse> {
19        let id = query
20            .get("providerId")
21            .and_then(JsValue::as_str)
22            .ok_or_else(|| crate::endpoint::validation("Missing providerId"))?;
23        let provider = self
24            .find_provider(id, ctx)
25            .await?
26            .ok_or_else(|| api(404, "No provider found for the given providerId"))?;
27        let config = provider
28            .saml_config
29            .as_deref()
30            .and_then(|config| serde_json::from_str::<Value>(config).ok())
31            .filter(|config| !config.is_null())
32            .ok_or_else(|| api(400, "Invalid SAML configuration"))?;
33        _ = super::sp_metadata_xml::from_config(&config)?;
34        if let Some(xml) = super::sp_metadata_xml::xml(&config) {
35            return Ok(EndpointResponse::from_http_response(
36                alibi_core::AuthResponse::text(200, xml)
37                    .with_header("content-type", "application/xml"),
38            ));
39        }
40        let entity = config
41            .get("spMetadata")
42            .and_then(|metadata| metadata.get("entityID"))
43            .and_then(Value::as_str)
44            .filter(|entity| !entity.is_empty())
45            .or_else(|| config.get("issuer").and_then(Value::as_str))
46            .unwrap_or(&provider.issuer);
47        let name_id = config
48            .get("identifierFormat")
49            .and_then(Value::as_str)
50            .filter(|format| !format.is_empty())
51            .unwrap_or("urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress");
52        let base = format!(
53            "{}{}",
54            ctx.config.base_url.trim_end_matches('/'),
55            ctx.config.base_path.trim_end_matches('/')
56        );
57        let acs = format!("{base}/sso/saml2/sp/acs/{id}");
58        let signed_assertions = if config.get("wantAssertionsSigned") == Some(&json!(true)) {
59            "true"
60        } else {
61            "false"
62        };
63        let signed_requests = if config.get("authnRequestsSigned") == Some(&json!(true)) {
64            "true"
65        } else {
66            "false"
67        };
68        let mut writer = Writer::new(Vec::new());
69        let mut root = BytesStart::new("EntityDescriptor");
70        _ = root.extend_attributes([
71            ("entityID", entity),
72            ("xmlns", "urn:oasis:names:tc:SAML:2.0:metadata"),
73            ("xmlns:assertion", "urn:oasis:names:tc:SAML:2.0:assertion"),
74            ("xmlns:ds", "http://www.w3.org/2000/09/xmldsig#"),
75        ]);
76        let mut descriptor = BytesStart::new("SPSSODescriptor");
77        _ = descriptor.extend_attributes([
78            ("AuthnRequestsSigned", signed_requests),
79            ("WantAssertionsSigned", signed_assertions),
80            (
81                "protocolSupportEnumeration",
82                "urn:oasis:names:tc:SAML:2.0:protocol",
83            ),
84        ]);
85        let mut consumer = BytesStart::new("AssertionConsumerService");
86        _ = consumer.extend_attributes([
87            ("index", "0"),
88            ("Binding", "urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST"),
89            ("Location", acs.as_str()),
90        ]);
91        for event in [
92            Event::Start(root),
93            Event::Start(descriptor),
94            Event::Start(BytesStart::new("NameIDFormat")),
95            Event::Text(BytesText::new(name_id)),
96            Event::End(BytesEnd::new("NameIDFormat")),
97            Event::Start(consumer),
98            Event::End(BytesEnd::new("AssertionConsumerService")),
99        ] {
100            writer
101                .write_event(event)
102                .map_err(|error| AuthError::internal(error.to_string()))?;
103        }
104        if self.config.saml_enable_single_logout {
105            let location = format!("{base}/sso/saml2/sp/slo/{id}");
106            for binding in ["HTTP-POST", "HTTP-Redirect"] {
107                let binding = format!("urn:oasis:names:tc:SAML:2.0:bindings:{binding}");
108                let mut service = BytesStart::new("SingleLogoutService");
109                _ = service.extend_attributes([
110                    ("Binding", binding.as_str()),
111                    ("Location", location.as_str()),
112                ]);
113                writer
114                    .write_event(Event::Start(service))
115                    .map_err(|error| AuthError::internal(error.to_string()))?;
116                writer
117                    .write_event(Event::End(BytesEnd::new("SingleLogoutService")))
118                    .map_err(|error| AuthError::internal(error.to_string()))?;
119            }
120        }
121        for name in ["SPSSODescriptor", "EntityDescriptor"] {
122            writer
123                .write_event(Event::End(BytesEnd::new(name)))
124                .map_err(|error| AuthError::internal(error.to_string()))?;
125        }
126        let xml = String::from_utf8(writer.into_inner())
127            .map_err(|error| AuthError::internal(error.to_string()))?;
128        Ok(EndpointResponse::from_http_response(
129            alibi_core::AuthResponse::text(200, &xml)
130                .with_header("content-type", "application/xml"),
131        ))
132    }
133}