flp_saml2/sp/
logout_redirect.rs1use base64::prelude::*;
18use chrono::{SecondsFormat, Utc};
19use flate2::{Compression, read::DeflateEncoder};
20use openssl::{hash::MessageDigest, sign::Signer};
21use quick_xml::se::to_string as to_xml_string;
22use rand::{Rng, rng};
23use serde::Serialize;
24use std::io::Read;
25use url::Url;
26
27use crate::{error::Result, idp::IdentityProvider, sp::ServiceProvider};
28
29#[derive(Serialize)]
30pub struct SessionIndex {
31 #[serde(rename = "$value")]
32 value: String,
33}
34
35#[derive(Serialize)]
36pub struct NameID {
37 #[serde(rename = "SPNameQualifier")]
38 sp_name_qualifier: String,
39 #[serde(rename = "Format")]
40 format: String,
41 #[serde(rename = "$value")]
42 value: String,
43}
44
45#[derive(Serialize)]
46pub struct Issuer {
47 #[serde(rename = "$value")]
48 value: String,
49}
50
51#[derive(Serialize)]
52#[serde(rename = "samlp:LogoutRequest")]
53pub struct LogoutRequest {
54 #[serde(rename = "xmlns:samlp")]
55 samlp: String,
56 #[serde(rename = "xmlns:saml")]
57 saml: String,
58 #[serde(rename = "ID")]
59 id: String,
60 #[serde(rename = "Version")]
61 version: String,
62 #[serde(rename = "IssueInstant")]
63 issue_instant: String,
64 #[serde(rename = "Destination")]
65 destination: String,
66 #[serde(rename = "saml:Issuer")]
67 issuer: Issuer,
68 #[serde(rename = "saml:NameID")]
69 name_id: NameID,
70 #[serde(rename = "samlp:SessionIndex")]
71 session_index: SessionIndex,
72}
73
74impl ServiceProvider {
75 pub fn logout_redirect(
76 &self,
77 idp: &IdentityProvider,
78 name_id: String,
79 session_index: String,
80 ) -> Result<Url> {
81 let random_bytes = rng().random::<[u8; 21]>();
82 let now = Utc::now();
83
84 let authn = to_xml_string(&LogoutRequest {
85 samlp: "urn:oasis:names:tc:SAML:2.0:protocol".into(),
86 saml: "urn:oasis:names:tc:SAML:2.0:assertion".into(),
87 id: format!("_{}", hex::encode(random_bytes)),
88 version: "2.0".into(),
89 issue_instant: now.to_rfc3339_opts(SecondsFormat::Millis, true),
90 destination: idp.logout.to_string(),
91 issuer: Issuer {
92 value: self.entity_id.to_string(),
93 },
94 name_id: NameID {
95 sp_name_qualifier: self.entity_id.to_string(),
96 format: self.name_id_format.clone().unwrap_or_else(|| {
97 "urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified".into()
98 }),
99 value: name_id,
100 },
101 session_index: SessionIndex {
102 value: session_index,
103 },
104 })
105 .unwrap();
106
107 let mut deflater = DeflateEncoder::new(authn.as_bytes(), Compression::fast());
108 let mut deflated = Vec::new();
109 deflater.read_to_end(&mut deflated)?;
110 let saml_request = BASE64_STANDARD.encode(deflated);
111 let mut url = idp.logout.clone();
112 url.query_pairs_mut()
113 .clear()
114 .append_pair("SAMLRequest", &saml_request);
115 if let Some(relay_state) = self.relay_state.as_ref() {
116 url.query_pairs_mut().append_pair("RelayState", relay_state);
117 }
118 url.query_pairs_mut().append_pair(
119 "SigAlg",
120 "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256",
121 );
122 let query_all = url.query().unwrap();
123 let mut signer = Signer::new(MessageDigest::sha256(), &self.private_key)?;
124 signer.update(query_all.as_bytes())?;
125 url.query_pairs_mut()
126 .append_pair("Signature", &BASE64_STANDARD.encode(signer.sign_to_vec()?));
127 Ok(url)
128 }
129}