1use core::marker::PhantomData;
2
3use super::bindings::{LogoutBinding, SsoRequestBinding, SsoResponseBinding};
4use super::endpoints::AcsEndpoint;
5use super::outbound::Outbound;
6use crate::config::EntityId;
7use crate::constants::Binding;
8use crate::error::SamlError;
9use crate::model::{
10 AuthnRequest, EndpointUrl, LogoutRequest, MessageId, RelayStateParam, SamlInstant,
11};
12
13mod sealed {
14 pub trait Sealed {}
15}
16
17#[doc(hidden)]
19pub trait PendingMessage: sealed::Sealed {
20 type Details: Clone + core::fmt::Debug + PartialEq + Eq;
22 type RequestBinding: Copy + core::fmt::Debug + PartialEq + Eq;
24 type ResponseBinding: Copy + core::fmt::Debug + PartialEq + Eq;
26}
27
28#[doc(hidden)]
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct AuthnPendingDetails {
32 request_binding: Option<SsoRequestBinding>,
33 response_binding: SsoResponseBinding,
34 acs: AcsEndpoint,
35}
36
37#[doc(hidden)]
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct LogoutPendingDetails {
41 binding: LogoutBinding,
42}
43
44impl sealed::Sealed for AuthnRequest {}
45
46impl PendingMessage for AuthnRequest {
47 type Details = AuthnPendingDetails;
48 type RequestBinding = SsoRequestBinding;
49 type ResponseBinding = SsoResponseBinding;
50}
51
52impl sealed::Sealed for LogoutRequest {}
53
54impl PendingMessage for LogoutRequest {
55 type Details = LogoutPendingDetails;
56 type RequestBinding = LogoutBinding;
57 type ResponseBinding = LogoutBinding;
58}
59
60#[derive(Debug, Clone, PartialEq, Eq)]
62pub struct PendingSnapshot<Message: PendingMessage> {
63 pub id: String,
65 pub relay_state: RelayStateParam,
67 pub peer_entity_id: String,
69 pub expected_binding: String,
71 pub request_binding: Option<String>,
73 pub acs_url: String,
78 pub acs_binding: String,
83 pub acs_index: Option<u16>,
88 pub acs_is_default: bool,
93 pub issued_at: Option<SamlInstant>,
95 pub expires_at: Option<SamlInstant>,
97 _message: PhantomData<Message>,
98}
99
100impl PendingSnapshot<AuthnRequest> {
101 pub fn authn_request(
123 id: impl Into<String>,
124 relay_state: RelayStateParam,
125 peer_entity_id: impl Into<String>,
126 expected_binding: impl Into<String>,
127 acs_url: impl Into<String>,
128 acs_binding: impl Into<String>,
129 ) -> Self {
130 Self {
131 id: id.into(),
132 relay_state,
133 peer_entity_id: peer_entity_id.into(),
134 expected_binding: expected_binding.into(),
135 request_binding: None,
136 acs_url: acs_url.into(),
137 acs_binding: acs_binding.into(),
138 acs_index: None,
139 acs_is_default: false,
140 issued_at: None,
141 expires_at: None,
142 _message: PhantomData,
143 }
144 }
145}
146
147impl PendingSnapshot<LogoutRequest> {
148 pub fn logout_request(
168 id: impl Into<String>,
169 relay_state: RelayStateParam,
170 peer_entity_id: impl Into<String>,
171 binding: LogoutBinding,
172 ) -> Self {
173 Self {
174 id: id.into(),
175 relay_state,
176 peer_entity_id: peer_entity_id.into(),
177 expected_binding: binding.as_binding().short_name().to_string(),
178 request_binding: Some(binding.as_binding().short_name().to_string()),
179 acs_url: String::new(),
180 acs_binding: String::new(),
181 acs_index: None,
182 acs_is_default: false,
183 issued_at: None,
184 expires_at: None,
185 _message: PhantomData,
186 }
187 }
188}
189
190#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct Pending<Message: PendingMessage> {
193 id: MessageId,
194 relay_state: RelayStateParam,
195 details: Message::Details,
196 peer_entity_id: EntityId,
197 issued_at: Option<SamlInstant>,
198 expires_at: Option<SamlInstant>,
199 _message: PhantomData<Message>,
200}
201
202pub type PendingAuthnRequest = Pending<AuthnRequest>;
204
205pub type PendingLogoutRequest = Pending<LogoutRequest>;
207
208impl<Message: PendingMessage> Pending<Message> {
209 fn validate_common(
210 relay_state: &RelayStateParam,
211 peer_entity_id: &EntityId,
212 ) -> Result<(), SamlError> {
213 relay_state.validate()?;
214 EntityId::try_new(peer_entity_id.as_str().to_string())?;
215 Ok(())
216 }
217
218 fn validate_snapshot_timing(
219 issued_at: &Option<SamlInstant>,
220 expires_at: &Option<SamlInstant>,
221 ) -> Result<(), SamlError> {
222 if expires_at.is_some() && issued_at.is_none() {
223 return Err(SamlError::Invalid(
224 "pending snapshot expiration requires an issue instant".into(),
225 ));
226 }
227 Ok(())
228 }
229
230 pub fn with_issue_instant(mut self, issued_at: SamlInstant) -> Self {
232 self.issued_at = Some(issued_at);
233 self
234 }
235
236 pub fn with_expiration(mut self, expires_at: SamlInstant) -> Self {
238 self.expires_at = Some(expires_at);
239 self
240 }
241
242 pub fn id(&self) -> &MessageId {
244 &self.id
245 }
246
247 pub fn relay_state(&self) -> &RelayStateParam {
249 &self.relay_state
250 }
251
252 pub fn peer_entity_id(&self) -> &EntityId {
254 &self.peer_entity_id
255 }
256
257 pub fn issued_at(&self) -> Option<&SamlInstant> {
259 self.issued_at.as_ref()
260 }
261
262 pub fn expires_at(&self) -> Option<&SamlInstant> {
264 self.expires_at.as_ref()
265 }
266}
267
268impl Pending<AuthnRequest> {
269 pub fn try_new(
277 request_id: MessageId,
278 relay_state: RelayStateParam,
279 acs: AcsEndpoint,
280 response_binding: SsoResponseBinding,
281 idp_entity_id: EntityId,
282 ) -> Result<Self, SamlError> {
283 Self::validate_common(&relay_state, &idp_entity_id)?;
284 if acs.binding() != response_binding {
285 return Err(SamlError::Invalid(
286 "ACS binding must match expected response binding".into(),
287 ));
288 }
289 Ok(Self {
290 id: request_id,
291 relay_state,
292 details: AuthnPendingDetails {
293 request_binding: None,
294 response_binding,
295 acs,
296 },
297 peer_entity_id: idp_entity_id,
298 issued_at: None,
299 expires_at: None,
300 _message: PhantomData,
301 })
302 }
303
304 pub fn with_request_binding(mut self, request_binding: SsoRequestBinding) -> Self {
306 self.details.request_binding = Some(request_binding);
307 self
308 }
309
310 pub fn from_snapshot(snapshot: PendingSnapshot<AuthnRequest>) -> Result<Self, SamlError> {
336 snapshot.relay_state.validate()?;
337 Self::validate_snapshot_timing(&snapshot.issued_at, &snapshot.expires_at)?;
338 let request_id = MessageId::try_new(snapshot.id)?;
339 let idp_entity_id = EntityId::try_new(snapshot.peer_entity_id)?;
340 let response_binding =
341 sso_response_binding_from_snapshot_value(&snapshot.expected_binding)?;
342 let acs_binding = sso_response_binding_from_snapshot_value(&snapshot.acs_binding)?;
343 let mut acs = AcsEndpoint::new(acs_binding, EndpointUrl::try_new(snapshot.acs_url)?)
344 .with_default_flag(snapshot.acs_is_default);
345 if let Some(index) = snapshot.acs_index {
346 acs = acs.with_index(index);
347 }
348 let request_binding = snapshot
349 .request_binding
350 .as_deref()
351 .map(sso_request_binding_from_snapshot_value)
352 .transpose()?;
353 let mut pending = Self::try_new(
354 request_id,
355 snapshot.relay_state,
356 acs,
357 response_binding,
358 idp_entity_id,
359 )?;
360 pending.details.request_binding = request_binding;
361 pending.issued_at = snapshot.issued_at;
362 pending.expires_at = snapshot.expires_at;
363 Ok(pending)
364 }
365
366 pub fn snapshot(&self) -> PendingSnapshot<AuthnRequest> {
368 let mut snapshot = PendingSnapshot::authn_request(
369 self.id.as_str(),
370 self.relay_state.clone(),
371 self.peer_entity_id.as_str(),
372 self.details.response_binding.as_binding().short_name(),
373 self.details.acs.location().as_str(),
374 self.details.acs.binding().as_binding().short_name(),
375 );
376 snapshot.request_binding = self
377 .details
378 .request_binding
379 .map(|binding| binding.as_binding().short_name().to_string());
380 snapshot.acs_index = self.details.acs.index();
381 snapshot.acs_is_default = self.details.acs.is_default();
382 snapshot.issued_at = self.issued_at.clone();
383 snapshot.expires_at = self.expires_at.clone();
384 snapshot
385 }
386
387 pub fn request_id(&self) -> &MessageId {
389 &self.id
390 }
391
392 pub fn request_binding(&self) -> Option<SsoRequestBinding> {
394 self.details.request_binding
395 }
396
397 pub fn response_binding(&self) -> SsoResponseBinding {
399 self.details.response_binding
400 }
401
402 pub fn acs(&self) -> &AcsEndpoint {
404 &self.details.acs
405 }
406
407 pub fn idp_entity_id(&self) -> &EntityId {
409 &self.peer_entity_id
410 }
411}
412
413impl Pending<LogoutRequest> {
414 pub fn try_new(
420 request_id: MessageId,
421 relay_state: RelayStateParam,
422 binding: LogoutBinding,
423 peer_entity_id: EntityId,
424 ) -> Result<Self, SamlError> {
425 Self::validate_common(&relay_state, &peer_entity_id)?;
426 Ok(Self {
427 id: request_id,
428 relay_state,
429 details: LogoutPendingDetails { binding },
430 peer_entity_id,
431 issued_at: None,
432 expires_at: None,
433 _message: PhantomData,
434 })
435 }
436
437 pub fn from_snapshot(snapshot: PendingSnapshot<LogoutRequest>) -> Result<Self, SamlError> {
463 snapshot.relay_state.validate()?;
464 Self::validate_snapshot_timing(&snapshot.issued_at, &snapshot.expires_at)?;
465 let request_id = MessageId::try_new(snapshot.id)?;
466 let peer_entity_id = EntityId::try_new(snapshot.peer_entity_id)?;
467 let response_binding = logout_binding_from_snapshot_value(&snapshot.expected_binding)?;
468 let request_binding = snapshot
469 .request_binding
470 .as_deref()
471 .map(logout_binding_from_snapshot_value)
472 .transpose()?
473 .unwrap_or(response_binding);
474 if request_binding != response_binding {
475 return Err(SamlError::Invalid(
476 "logout request and response bindings must match".into(),
477 ));
478 }
479 let mut pending = Self::try_new(
480 request_id,
481 snapshot.relay_state,
482 response_binding,
483 peer_entity_id,
484 )?;
485 pending.issued_at = snapshot.issued_at;
486 pending.expires_at = snapshot.expires_at;
487 Ok(pending)
488 }
489
490 pub fn snapshot(&self) -> PendingSnapshot<LogoutRequest> {
492 let mut snapshot = PendingSnapshot::logout_request(
493 self.id.as_str(),
494 self.relay_state.clone(),
495 self.peer_entity_id.as_str(),
496 self.details.binding,
497 );
498 snapshot.issued_at = self.issued_at.clone();
499 snapshot.expires_at = self.expires_at.clone();
500 snapshot
501 }
502
503 pub fn request_binding(&self) -> LogoutBinding {
505 self.details.binding
506 }
507
508 pub fn response_binding(&self) -> LogoutBinding {
510 self.details.binding
511 }
512}
513
514fn sso_response_binding_from_snapshot_value(value: &str) -> Result<SsoResponseBinding, SamlError> {
515 let binding = Binding::from_short_name(value)
516 .or_else(|| Binding::from_urn(value))
517 .ok_or(SamlError::UndefinedBinding)?;
518 SsoResponseBinding::try_from(binding)
519}
520
521fn sso_request_binding_from_snapshot_value(value: &str) -> Result<SsoRequestBinding, SamlError> {
522 let binding = Binding::from_short_name(value)
523 .or_else(|| Binding::from_urn(value))
524 .ok_or(SamlError::UndefinedBinding)?;
525 SsoRequestBinding::try_from(binding)
526}
527
528fn logout_binding_from_snapshot_value(value: &str) -> Result<LogoutBinding, SamlError> {
529 let binding = Binding::from_short_name(value)
530 .or_else(|| Binding::from_urn(value))
531 .ok_or(SamlError::UndefinedBinding)?;
532 LogoutBinding::try_from(binding)
533}
534
535#[derive(Debug, Clone)]
537pub struct Started<Message: PendingMessage> {
538 pub pending: Pending<Message>,
540 pub outbound: Outbound<Message>,
542}