1use crate::browser::{BrowserInput, LogoutBinding, Outbound, PendingLogoutRequest, Started};
2use crate::config::{EntityId, IdpDescriptor, SpDescriptor};
3use crate::constants::Binding;
4use crate::entity::{capture_idp_issuance_window, now_iso8601, EntitySetting};
5use crate::error::SamlError as Error;
6use crate::flow::HttpRequest;
7use crate::logout::{
8 create_logout_request_with_session_indexes, create_logout_response, parse_logout_request_at,
9 parse_logout_response_at, LogoutRequestSessionIndexes, LogoutRequestValidation,
10};
11use crate::metadata::Metadata;
12use crate::model::{
13 LogoutCompleted, LogoutRequest, LogoutResponse, LogoutSubject, Received, ReplayKey,
14 SamlInstant, SamlValidationContext,
15};
16
17use super::raw_mapping::{
18 ensure_entity_id, ensure_relay_state, input_binding, raw_idp_descriptor, raw_sp_descriptor,
19 relay_state_from_input,
20};
21use super::{Idp, LogoutSigning, RespondSlo, Saml, SamlError, Sp, StartSlo};
22
23impl Saml<Sp> {
24 pub fn start_slo(
49 &self,
50 idp: &IdpDescriptor,
51 subject: LogoutSubject,
52 options: StartSlo,
53 ) -> Result<Started<LogoutRequest>, SamlError> {
54 let raw_idp = raw_idp_descriptor(idp)?;
55 start_slo_impl(
56 &self.raw_service_provider().setting,
57 &self.raw_service_provider().metadata,
58 idp.entity_id(),
59 &raw_idp.metadata,
60 subject,
61 options,
62 StartSloRole::SessionParticipant,
63 )
64 }
65
66 pub fn receive_slo(
78 &self,
79 idp: &IdpDescriptor,
80 input: BrowserInput<LogoutRequest>,
81 validation: SamlValidationContext<'_>,
82 ) -> Result<Received<LogoutRequest>, SamlError> {
83 let raw_idp = raw_idp_descriptor(idp)?;
84 receive_slo_impl(
85 &self.raw_service_provider().setting,
86 &self.raw_service_provider().metadata,
87 &raw_idp.metadata,
88 input,
89 validation,
90 )
91 }
92
93 pub fn respond_slo(
101 &self,
102 idp: &IdpDescriptor,
103 request: &Received<LogoutRequest>,
104 options: RespondSlo,
105 ) -> Result<Outbound<LogoutResponse>, SamlError> {
106 let raw_idp = raw_idp_descriptor(idp)?;
107 respond_slo_impl(
108 &self.raw_service_provider().setting,
109 &self.raw_service_provider().metadata,
110 &raw_idp.metadata,
111 request,
112 options,
113 )
114 }
115
116 pub fn finish_slo(
157 &self,
158 idp: &IdpDescriptor,
159 pending: &PendingLogoutRequest,
160 input: BrowserInput<LogoutResponse>,
161 validation: SamlValidationContext<'_>,
162 ) -> Result<LogoutCompleted, SamlError> {
163 let raw_idp = raw_idp_descriptor(idp)?;
164 finish_slo_impl(
165 &self.raw_service_provider().setting,
166 &self.raw_service_provider().metadata,
167 idp.entity_id(),
168 &raw_idp.metadata,
169 pending,
170 input,
171 validation,
172 )
173 }
174}
175
176impl Saml<Idp> {
177 pub fn start_slo(
208 &self,
209 sp: &SpDescriptor,
210 subject: LogoutSubject,
211 options: StartSlo,
212 ) -> Result<Started<LogoutRequest>, SamlError> {
213 let raw_sp = raw_sp_descriptor(sp)?;
214 start_slo_impl(
215 &self.raw_identity_provider().setting,
216 &self.raw_identity_provider().metadata,
217 sp.entity_id(),
218 &raw_sp.metadata,
219 subject,
220 options,
221 StartSloRole::SessionAuthority {
222 issuance_lifetime: self.0.issuance_lifetime,
223 },
224 )
225 }
226
227 pub fn receive_slo(
266 &self,
267 sp: &SpDescriptor,
268 input: BrowserInput<LogoutRequest>,
269 validation: SamlValidationContext<'_>,
270 ) -> Result<Received<LogoutRequest>, SamlError> {
271 let raw_sp = raw_sp_descriptor(sp)?;
272 receive_slo_impl(
273 &self.raw_identity_provider().setting,
274 &self.raw_identity_provider().metadata,
275 &raw_sp.metadata,
276 input,
277 validation,
278 )
279 }
280
281 pub fn respond_slo(
305 &self,
306 sp: &SpDescriptor,
307 request: &Received<LogoutRequest>,
308 options: RespondSlo,
309 ) -> Result<Outbound<LogoutResponse>, SamlError> {
310 let raw_sp = raw_sp_descriptor(sp)?;
311 respond_slo_impl(
312 &self.raw_identity_provider().setting,
313 &self.raw_identity_provider().metadata,
314 &raw_sp.metadata,
315 request,
316 options,
317 )
318 }
319
320 pub fn finish_slo(
361 &self,
362 sp: &SpDescriptor,
363 pending: &PendingLogoutRequest,
364 input: BrowserInput<LogoutResponse>,
365 validation: SamlValidationContext<'_>,
366 ) -> Result<LogoutCompleted, SamlError> {
367 let raw_sp = raw_sp_descriptor(sp)?;
368 finish_slo_impl(
369 &self.raw_identity_provider().setting,
370 &self.raw_identity_provider().metadata,
371 sp.entity_id(),
372 &raw_sp.metadata,
373 pending,
374 input,
375 validation,
376 )
377 }
378}
379
380struct TypedLogoutSubject {
381 name_id: String,
382 session_indexes: Vec<String>,
383}
384
385#[derive(Debug, Clone, Copy)]
386enum StartSloRole {
387 SessionParticipant,
388 SessionAuthority { issuance_lifetime: time::Duration },
389}
390
391fn start_slo_impl(
392 local_setting: &EntitySetting,
393 local_metadata: &Metadata,
394 peer_entity_id: &EntityId,
395 peer_metadata: &Metadata,
396 subject: LogoutSubject,
397 options: StartSlo,
398 role: StartSloRole,
399) -> Result<Started<LogoutRequest>, SamlError> {
400 options.relay_state.validate()?;
401 let subject = typed_logout_subject(subject);
402 let (issue_instant, not_on_or_after, request_validation) = match role {
403 StartSloRole::SessionParticipant => {
404 (now_iso8601(), None, LogoutRequestValidation::Compatibility)
405 }
406 StartSloRole::SessionAuthority { issuance_lifetime } => {
407 let window = capture_idp_issuance_window(issuance_lifetime)?;
408 (
409 window.issue_instant,
410 Some(window.expiration),
411 LogoutRequestValidation::SessionAuthority,
412 )
413 }
414 };
415 let created = create_logout_request_with_session_indexes(LogoutRequestSessionIndexes {
416 init_setting: local_setting,
417 init_meta: local_metadata,
418 target_meta: peer_metadata,
419 binding: options.binding.as_binding(),
420 name_id: &subject.name_id,
421 session_indexes: &subject.session_indexes,
422 relay_state: options.relay_state.as_deref(),
423 want_signed: logout_request_signing(local_setting, options.signing),
424 issue_instant: &issue_instant,
425 not_on_or_after: not_on_or_after.as_deref(),
426 validation: request_validation,
427 })?;
428 let outbound = Outbound::<LogoutRequest>::try_from(created.context)?;
429 let mut pending = PendingLogoutRequest::try_new(
430 outbound.id().clone(),
431 options.relay_state,
432 options.binding,
433 peer_entity_id.clone(),
434 )?;
435 if matches!(role, StartSloRole::SessionAuthority { .. }) {
436 pending = pending.with_issue_instant(SamlInstant::try_new(created.issue_instant)?);
437 let expiration = created.not_on_or_after.ok_or_else(|| {
438 SamlError::Invalid(
439 "Session Authority LogoutRequest is missing its generated expiration".into(),
440 )
441 })?;
442 pending = pending.with_expiration(SamlInstant::try_new(expiration)?);
443 }
444 Ok(Started { pending, outbound })
445}
446
447fn receive_slo_impl(
448 local_setting: &EntitySetting,
449 local_metadata: &Metadata,
450 peer_metadata: &Metadata,
451 input: BrowserInput<LogoutRequest>,
452 mut validation: SamlValidationContext<'_>,
453) -> Result<Received<LogoutRequest>, SamlError> {
454 let relay_state = relay_state_from_input(&input)?;
455 let binding = LogoutBinding::try_from(input_binding(&input))?;
456 let request = HttpRequest::try_from(input)?;
457 let flow = parse_logout_request_at(
458 local_setting,
459 peer_metadata,
460 binding.as_binding(),
461 &request,
462 validation.now(),
463 validation.clock_skew().as_millis(),
464 )?;
465 let replay_deadline = crate::validator::logout_request_not_on_or_after_deadline(
466 &flow.extract,
467 validation.now_offset()?,
468 validation.clock_skew().not_on_or_after_millis(),
469 )?;
470 let logout = LogoutRequest::try_from(flow)?;
471 ensure_logout_destination(local_metadata, binding, logout.destination())?;
472 validation.check_and_store_message_replay_until(
473 ReplayKey::LogoutRequestId(logout.id().clone()),
474 replay_deadline,
475 )?;
476 Ok(Received::new(logout).with_relay_state(relay_state))
477}
478
479fn respond_slo_impl(
480 local_setting: &EntitySetting,
481 local_metadata: &Metadata,
482 peer_metadata: &Metadata,
483 request: &Received<LogoutRequest>,
484 options: RespondSlo,
485) -> Result<Outbound<LogoutResponse>, SamlError> {
486 let relay_state = options
487 .relay_state
488 .unwrap_or_else(|| request.relay_state().clone());
489 relay_state.validate()?;
490 let context = create_logout_response(
491 local_setting,
492 local_metadata,
493 peer_metadata,
494 options.binding.as_binding(),
495 Some(request.message().id().as_str()),
496 relay_state.as_deref(),
497 true,
500 )?;
501 Outbound::<LogoutResponse>::try_from(context)
502}
503
504fn finish_slo_impl(
505 local_setting: &EntitySetting,
506 local_metadata: &Metadata,
507 peer_entity_id: &EntityId,
508 peer_metadata: &Metadata,
509 pending: &PendingLogoutRequest,
510 input: BrowserInput<LogoutResponse>,
511 mut validation: SamlValidationContext<'_>,
512) -> Result<LogoutCompleted, SamlError> {
513 ensure_entity_id(pending.peer_entity_id(), peer_entity_id)?;
514 ensure_logout_response_binding(input_binding(&input), pending.response_binding())?;
515 ensure_relay_state(pending.relay_state(), &relay_state_from_input(&input)?)?;
516 let request = HttpRequest::try_from(input)?;
517 let flow = parse_logout_response_at(
518 local_setting,
519 peer_metadata,
520 pending.response_binding().as_binding(),
521 &request,
522 pending.id().as_str(),
523 validation.now(),
524 validation.clock_skew().as_millis(),
525 )?;
526 let response = LogoutResponse::try_from(flow)?;
527 ensure_logout_destination(
528 local_metadata,
529 pending.response_binding(),
530 response.destination(),
531 )?;
532 validation
533 .check_and_store_message_replay(ReplayKey::LogoutResponseId(response.id().clone()))?;
534 Ok(LogoutCompleted::from_response(
535 peer_entity_id.clone(),
536 response,
537 ))
538}
539
540fn logout_request_signing(setting: &EntitySetting, signing: LogoutSigning) -> bool {
541 match signing {
542 LogoutSigning::FollowLocalPolicy => setting.want_logout_request_signed,
543 LogoutSigning::Sign => true,
544 LogoutSigning::DoNotSignForCompatibility => false,
545 }
546}
547
548fn ensure_logout_response_binding(
549 actual: Binding,
550 expected: LogoutBinding,
551) -> Result<(), SamlError> {
552 if actual == expected.as_binding() {
553 return Ok(());
554 }
555 Err(Error::UnsupportedBinding { binding: actual })
556}
557
558fn ensure_logout_destination(
559 local_metadata: &Metadata,
560 binding: LogoutBinding,
561 actual: Option<&crate::model::EndpointUrl>,
562) -> Result<(), SamlError> {
563 let Some(actual) = actual else {
564 return Ok(());
565 };
566 let expected = local_metadata
567 .get_single_logout_service(binding.as_binding())
568 .ok_or_else(|| Error::MissingMetadata("SingleLogoutService".into()))?;
569 if actual.as_str() == expected {
570 return Ok(());
571 }
572 Err(Error::destination_mismatch(
573 &expected,
574 Some(actual.as_str()),
575 ))
576}
577
578fn typed_logout_subject(subject: LogoutSubject) -> TypedLogoutSubject {
579 TypedLogoutSubject {
580 name_id: subject.name_id().value().to_string(),
581 session_indexes: subject
582 .session_indexes()
583 .iter()
584 .map(|session_index| session_index.as_str().to_string())
585 .collect(),
586 }
587}