1use super::extract::{
2 optional_endpoint, optional_request_id, required_str, session_indexes_from_value,
3};
4use super::identifiers::{MessageId, SamlInstant, SessionIndex};
5use super::subject::NameId;
6use super::EndpointUrl;
7use crate::config::EntityId;
8use crate::constants::status_code;
9use crate::error::SamlError;
10use crate::raw::FlowResult;
11use crate::xml::parse_saml_utc_date_time;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct LogoutSubject {
16 name_id: NameId,
17 session_indexes: Vec<SessionIndex>,
18}
19
20impl LogoutSubject {
21 pub fn new(name_id: NameId, session_indexes: Vec<SessionIndex>) -> Self {
23 Self {
24 name_id,
25 session_indexes,
26 }
27 }
28
29 pub fn from_name_id(name_id: NameId) -> Self {
31 Self::new(name_id, Vec::new())
32 }
33
34 pub fn with_session_index(name_id: NameId, session_index: SessionIndex) -> Self {
36 Self::new(name_id, vec![session_index])
37 }
38
39 pub fn name_id(&self) -> &NameId {
41 &self.name_id
42 }
43
44 pub fn session_index(&self) -> Option<&SessionIndex> {
46 self.session_indexes.first()
47 }
48
49 pub fn session_indexes(&self) -> &[SessionIndex] {
51 &self.session_indexes
52 }
53}
54
55#[derive(Debug, Clone)]
57pub struct LogoutRequest {
58 id: MessageId,
59 issue_instant: SamlInstant,
60 not_on_or_after: Option<SamlInstant>,
61 issuer: EntityId,
62 name_id: Option<NameId>,
63 session_indexes: Vec<SessionIndex>,
64 destination: Option<EndpointUrl>,
65 raw_flow: FlowResult,
66}
67
68impl LogoutRequest {
69 pub fn id(&self) -> &MessageId {
71 &self.id
72 }
73
74 pub fn issue_instant(&self) -> &SamlInstant {
76 &self.issue_instant
77 }
78
79 pub fn not_on_or_after(&self) -> Option<&SamlInstant> {
81 self.not_on_or_after.as_ref()
82 }
83
84 pub fn issuer(&self) -> &EntityId {
86 &self.issuer
87 }
88
89 pub fn name_id(&self) -> Option<&NameId> {
91 self.name_id.as_ref()
92 }
93
94 pub fn session_indexes(&self) -> &[SessionIndex] {
96 &self.session_indexes
97 }
98
99 pub fn destination(&self) -> Option<&EndpointUrl> {
101 self.destination.as_ref()
102 }
103
104 pub fn raw_flow(&self) -> &FlowResult {
109 &self.raw_flow
110 }
111}
112
113impl TryFrom<FlowResult> for LogoutRequest {
114 type Error = SamlError;
115
116 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
117 let id = MessageId::try_new(required_str(&raw_flow.extract, "request.id")?)?;
118 let issue_instant = logout_request_instant_from_extract(
119 &raw_flow.extract,
120 "issueInstant",
121 "IssueInstant",
122 true,
123 )?
124 .ok_or_else(|| {
125 SamlError::ProtocolProfile(
126 "LogoutRequest is missing required unqualified attribute IssueInstant".into(),
127 )
128 })?;
129 let not_on_or_after = logout_request_instant_from_extract(
130 &raw_flow.extract,
131 "notOnOrAfter",
132 "NotOnOrAfter",
133 false,
134 )?;
135 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
136 let name_id = raw_flow
137 .extract
138 .get_str("nameID")
139 .map(|value| NameId::new(value, None));
140 let session_indexes = session_indexes_from_value(raw_flow.extract.get("sessionIndex"))?;
141 let destination = optional_endpoint(&raw_flow.extract, "request.destination")?;
142 Ok(Self {
143 id,
144 issue_instant,
145 not_on_or_after,
146 issuer,
147 name_id,
148 session_indexes,
149 destination,
150 raw_flow,
151 })
152 }
153}
154
155fn logout_request_instant_from_extract(
156 extract: &crate::util::Value,
157 extract_field: &str,
158 attribute: &str,
159 required: bool,
160) -> Result<Option<SamlInstant>, SamlError> {
161 let path = format!("request.{extract_field}");
162 let Some(value) = extract.get_str(&path) else {
163 if required {
164 return Err(SamlError::ProtocolProfile(format!(
165 "LogoutRequest is missing required unqualified attribute {attribute}"
166 )));
167 }
168 return Ok(None);
169 };
170 let normalized = parse_saml_utc_date_time(value).ok_or_else(|| {
171 SamlError::ProtocolProfile(format!(
172 "LogoutRequest {attribute} must use the SAML-conformant UTC xs:dateTime form ending in Z"
173 ))
174 })?;
175 SamlInstant::try_new(normalized).map(Some)
176}
177
178#[derive(Debug, Clone)]
180pub struct LogoutResponse {
181 id: MessageId,
182 issuer: EntityId,
183 issue_instant: SamlInstant,
184 in_response_to: Option<MessageId>,
185 destination: Option<EndpointUrl>,
186 raw_flow: FlowResult,
187}
188
189impl LogoutResponse {
190 pub fn id(&self) -> &MessageId {
192 &self.id
193 }
194
195 pub fn issuer(&self) -> &EntityId {
197 &self.issuer
198 }
199
200 pub fn issue_instant(&self) -> &SamlInstant {
202 &self.issue_instant
203 }
204
205 pub fn in_response_to(&self) -> Option<&MessageId> {
207 self.in_response_to.as_ref()
208 }
209
210 pub fn destination(&self) -> Option<&EndpointUrl> {
212 self.destination.as_ref()
213 }
214
215 pub fn raw_flow(&self) -> &FlowResult {
220 &self.raw_flow
221 }
222}
223
224impl TryFrom<FlowResult> for LogoutResponse {
225 type Error = SamlError;
226
227 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
228 let id = MessageId::try_new(required_str(&raw_flow.extract, "response.id")?)?;
229 let issue_instant = issue_instant_from_extract(&raw_flow.extract)?;
230 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
231 let in_response_to = optional_request_id(&raw_flow.extract, "response.inResponseTo")?;
232 let destination = optional_endpoint(&raw_flow.extract, "response.destination")?;
233 Ok(Self {
234 id,
235 issuer,
236 issue_instant,
237 in_response_to,
238 destination,
239 raw_flow,
240 })
241 }
242}
243
244fn issue_instant_from_extract(extract: &crate::util::Value) -> Result<SamlInstant, SamlError> {
245 let issue_instant = extract.get_str("response.issueInstant").ok_or_else(|| {
246 SamlError::ProtocolProfile(
247 "LogoutResponse is missing required unqualified attribute IssueInstant".into(),
248 )
249 })?;
250 let issue_instant = parse_saml_utc_date_time(issue_instant).ok_or_else(|| {
251 SamlError::ProtocolProfile(
252 "LogoutResponse IssueInstant must use the SAML-conformant UTC xs:dateTime form ending in Z"
253 .into(),
254 )
255 })?;
256 SamlInstant::try_new(issue_instant)
257}
258
259#[derive(Debug, Clone)]
261pub struct LogoutCompleted {
262 peer_entity_id: EntityId,
263 response: Option<LogoutResponse>,
264}
265
266impl PartialEq for LogoutCompleted {
267 fn eq(&self, other: &Self) -> bool {
268 self.peer_entity_id == other.peer_entity_id
269 }
270}
271
272impl Eq for LogoutCompleted {}
273
274impl LogoutCompleted {
275 pub fn new(peer_entity_id: EntityId) -> Self {
277 Self {
278 peer_entity_id,
279 response: None,
280 }
281 }
282
283 pub fn from_response(peer_entity_id: EntityId, response: LogoutResponse) -> Self {
285 Self {
286 peer_entity_id,
287 response: Some(response),
288 }
289 }
290
291 pub fn peer_entity_id(&self) -> &EntityId {
293 &self.peer_entity_id
294 }
295
296 pub fn response(&self) -> Option<&LogoutResponse> {
298 self.response.as_ref()
299 }
300
301 pub fn status(&self) -> Option<&str> {
303 self.response.as_ref().map(|_| status_code::SUCCESS)
304 }
305
306 pub fn raw_flow(&self) -> Option<&FlowResult> {
308 self.response.as_ref().map(LogoutResponse::raw_flow)
309 }
310}