alibi_plugins/sso/
sp_metadata.rs1use 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}