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_with_status,
9 parse_logout_request_at, parse_logout_response_at, LogoutFlowValidation,
10 LogoutRequestSessionIndexes, LogoutRequestValidation,
11};
12use crate::metadata::Metadata;
13use crate::model::{
14 LogoutCompleted, LogoutRequest, LogoutResponse, LogoutSubject, Received, ReplayKey,
15 SamlInstant, SamlValidationContext, Status,
16};
17
18use super::raw_mapping::{
19 ensure_entity_id, ensure_relay_state, input_binding, raw_idp_descriptor, raw_sp_descriptor,
20 relay_state_from_input,
21};
22use super::{Idp, LogoutSigning, RespondSlo, Saml, SamlError, Sp, StartSlo};
23
24impl Saml<Sp> {
25 pub fn start_slo(
54 &self,
55 idp: &IdpDescriptor,
56 subject: LogoutSubject,
57 options: StartSlo,
58 ) -> Result<Started<LogoutRequest>, SamlError> {
59 let raw_idp = raw_idp_descriptor(idp)?;
60 start_slo_impl(
61 &self.raw_service_provider().setting,
62 &self.raw_service_provider().metadata,
63 idp.entity_id(),
64 &raw_idp.metadata,
65 subject,
66 options,
67 StartSloRole::SessionParticipant,
68 )
69 }
70
71 pub fn receive_slo(
83 &self,
84 idp: &IdpDescriptor,
85 input: BrowserInput<LogoutRequest>,
86 validation: SamlValidationContext<'_>,
87 ) -> Result<Received<LogoutRequest>, SamlError> {
88 let raw_idp = raw_idp_descriptor(idp)?;
89 receive_slo_impl(
90 &self.raw_service_provider().setting,
91 &self.raw_service_provider().metadata,
92 &raw_idp.metadata,
93 input,
94 validation,
95 )
96 }
97
98 pub fn respond_slo(
110 &self,
111 idp: &IdpDescriptor,
112 request: &Received<LogoutRequest>,
113 options: RespondSlo,
114 ) -> Result<Outbound<LogoutResponse>, SamlError> {
115 let raw_idp = raw_idp_descriptor(idp)?;
116 respond_slo_impl(
117 &self.raw_service_provider().setting,
118 &self.raw_service_provider().metadata,
119 &raw_idp.metadata,
120 request,
121 options,
122 LogoutResponseTransport::SessionParticipant,
123 )
124 }
125
126 pub fn finish_slo(
169 &self,
170 idp: &IdpDescriptor,
171 pending: &PendingLogoutRequest,
172 input: BrowserInput<LogoutResponse>,
173 validation: SamlValidationContext<'_>,
174 ) -> Result<LogoutCompleted, SamlError> {
175 let raw_idp = raw_idp_descriptor(idp)?;
176 finish_slo_impl(
177 &self.raw_service_provider().setting,
178 &self.raw_service_provider().metadata,
179 idp.entity_id(),
180 &raw_idp.metadata,
181 pending,
182 input,
183 validation,
184 )
185 }
186}
187
188impl Saml<Idp> {
189 pub fn start_slo(
224 &self,
225 sp: &SpDescriptor,
226 subject: LogoutSubject,
227 options: StartSlo,
228 ) -> Result<Started<LogoutRequest>, SamlError> {
229 let raw_sp = raw_sp_descriptor(sp)?;
230 start_slo_impl(
231 &self.raw_identity_provider().setting,
232 &self.raw_identity_provider().metadata,
233 sp.entity_id(),
234 &raw_sp.metadata,
235 subject,
236 options,
237 StartSloRole::SessionAuthority {
238 issuance_lifetime: self.0.issuance_lifetime,
239 },
240 )
241 }
242
243 pub fn receive_slo(
282 &self,
283 sp: &SpDescriptor,
284 input: BrowserInput<LogoutRequest>,
285 validation: SamlValidationContext<'_>,
286 ) -> Result<Received<LogoutRequest>, SamlError> {
287 let raw_sp = raw_sp_descriptor(sp)?;
288 receive_slo_impl(
289 &self.raw_identity_provider().setting,
290 &self.raw_identity_provider().metadata,
291 &raw_sp.metadata,
292 input,
293 validation,
294 )
295 }
296
297 pub fn respond_slo(
323 &self,
324 sp: &SpDescriptor,
325 request: &Received<LogoutRequest>,
326 options: RespondSlo,
327 ) -> Result<Outbound<LogoutResponse>, SamlError> {
328 let raw_sp = raw_sp_descriptor(sp)?;
329 respond_slo_impl(
330 &self.raw_identity_provider().setting,
331 &self.raw_identity_provider().metadata,
332 &raw_sp.metadata,
333 request,
334 options,
335 LogoutResponseTransport::SessionAuthority,
336 )
337 }
338
339 pub fn finish_slo(
382 &self,
383 sp: &SpDescriptor,
384 pending: &PendingLogoutRequest,
385 input: BrowserInput<LogoutResponse>,
386 validation: SamlValidationContext<'_>,
387 ) -> Result<LogoutCompleted, SamlError> {
388 let raw_sp = raw_sp_descriptor(sp)?;
389 finish_slo_impl(
390 &self.raw_identity_provider().setting,
391 &self.raw_identity_provider().metadata,
392 sp.entity_id(),
393 &raw_sp.metadata,
394 pending,
395 input,
396 validation,
397 )
398 }
399}
400
401struct TypedLogoutSubject {
402 name_id: String,
403 session_indexes: Vec<String>,
404}
405
406#[derive(Debug, Clone, Copy)]
407enum StartSloRole {
408 SessionParticipant,
409 SessionAuthority { issuance_lifetime: time::Duration },
410}
411
412fn start_slo_impl(
413 local_setting: &EntitySetting,
414 local_metadata: &Metadata,
415 peer_entity_id: &EntityId,
416 peer_metadata: &Metadata,
417 subject: LogoutSubject,
418 options: StartSlo,
419 role: StartSloRole,
420) -> Result<Started<LogoutRequest>, SamlError> {
421 options.relay_state.validate()?;
422 let subject = typed_logout_subject(subject);
423 let follows_rules = options.follows_generation_rules();
424 if follows_rules && matches!(options.signing, LogoutSigning::DoNotSignForCompatibility) {
425 return Err(Error::ProtocolProfile(
426 "a LogoutRequest signature cannot be disabled for HTTP-Redirect, HTTP-POST, or HTTP-POST-SimpleSign when Single Logout generation rules are selected".into(),
427 ));
428 }
429 if matches!(role, StartSloRole::SessionParticipant) {
430 enforce_participant_https(
431 peer_metadata,
432 options.binding.as_binding(),
433 follows_rules,
434 options.allows_http(),
435 )?;
436 }
437 let (issue_instant, not_on_or_after, request_validation) = match role {
438 StartSloRole::SessionParticipant => (
439 now_iso8601(),
440 None,
441 if follows_rules {
442 LogoutRequestValidation::SessionParticipant
443 } else {
444 LogoutRequestValidation::Compatibility
445 },
446 ),
447 StartSloRole::SessionAuthority { issuance_lifetime } => {
448 let window = capture_idp_issuance_window(issuance_lifetime)?;
449 (
450 window.issue_instant,
451 Some(window.expiration),
452 LogoutRequestValidation::SessionAuthority,
453 )
454 }
455 };
456 let want_signed = if follows_rules {
457 true
458 } else {
459 logout_request_signing(local_setting, options.signing)
460 };
461 let created = create_logout_request_with_session_indexes(LogoutRequestSessionIndexes {
462 init_setting: local_setting,
463 init_meta: local_metadata,
464 target_meta: peer_metadata,
465 binding: options.binding.as_binding(),
466 name_id: &subject.name_id,
467 session_indexes: &subject.session_indexes,
468 relay_state: options.relay_state.as_deref(),
469 want_signed,
470 issue_instant: &issue_instant,
471 not_on_or_after: not_on_or_after.as_deref(),
472 validation: request_validation,
473 })?;
474 let outbound = Outbound::<LogoutRequest>::try_from(created.context)?;
475 let mut pending = PendingLogoutRequest::try_new(
476 outbound.id().clone(),
477 options.relay_state,
478 options.binding,
479 peer_entity_id.clone(),
480 )?;
481 if matches!(role, StartSloRole::SessionAuthority { .. }) {
482 pending = pending.with_issue_instant(SamlInstant::try_new(created.issue_instant)?);
483 let expiration = created.not_on_or_after.ok_or_else(|| {
484 SamlError::Invalid(
485 "Session Authority LogoutRequest is missing its generated expiration".into(),
486 )
487 })?;
488 pending = pending.with_expiration(SamlInstant::try_new(expiration)?);
489 }
490 Ok(Started { pending, outbound })
491}
492
493fn receive_slo_impl(
494 local_setting: &EntitySetting,
495 local_metadata: &Metadata,
496 peer_metadata: &Metadata,
497 input: BrowserInput<LogoutRequest>,
498 mut validation: SamlValidationContext<'_>,
499) -> Result<Received<LogoutRequest>, SamlError> {
500 let relay_state = relay_state_from_input(&input)?;
501 let binding = LogoutBinding::try_from(input_binding(&input))?;
502 let expected_recipient = logout_recipient_endpoint(local_metadata, binding)?;
503 let request = HttpRequest::try_from(input)?;
504 let flow = parse_logout_request_at(
505 local_setting,
506 peer_metadata,
507 binding.as_binding(),
508 &request,
509 LogoutFlowValidation::typed(
510 &expected_recipient,
511 validation.now(),
512 validation.clock_skew().as_millis(),
513 ),
514 )?;
515 let replay_deadline = crate::validator::logout_request_not_on_or_after_deadline(
516 &flow.extract,
517 validation.now_offset()?,
518 validation.clock_skew().not_on_or_after_millis(),
519 )?;
520 let logout = LogoutRequest::try_from(flow)?;
521 validation.check_and_store_message_replay_until(
522 ReplayKey::LogoutRequestId(logout.id().clone()),
523 replay_deadline,
524 )?;
525 Ok(Received::new(logout).with_relay_state(relay_state))
526}
527
528fn respond_slo_impl(
529 local_setting: &EntitySetting,
530 local_metadata: &Metadata,
531 peer_metadata: &Metadata,
532 request: &Received<LogoutRequest>,
533 options: RespondSlo,
534 transport: LogoutResponseTransport,
535) -> Result<Outbound<LogoutResponse>, SamlError> {
536 if matches!(transport, LogoutResponseTransport::SessionParticipant) {
537 enforce_participant_https(
538 peer_metadata,
539 options.binding.as_binding(),
540 options.follows_generation_rules(),
541 options.allows_http(),
542 )?;
543 }
544 let relay_state = options
545 .relay_state
546 .unwrap_or_else(|| request.relay_state().clone());
547 relay_state.validate()?;
548 let status = options.status.clone().unwrap_or_else(Status::success);
549 let context = create_logout_response_with_status(
550 local_setting,
551 local_metadata,
552 peer_metadata,
553 options.binding.as_binding(),
554 Some(request.message().id().as_str()),
555 relay_state.as_deref(),
556 true,
559 None,
560 &status,
561 )?;
562 Outbound::<LogoutResponse>::try_from(context)
563}
564
565fn finish_slo_impl(
566 local_setting: &EntitySetting,
567 local_metadata: &Metadata,
568 peer_entity_id: &EntityId,
569 peer_metadata: &Metadata,
570 pending: &PendingLogoutRequest,
571 input: BrowserInput<LogoutResponse>,
572 mut validation: SamlValidationContext<'_>,
573) -> Result<LogoutCompleted, SamlError> {
574 ensure_entity_id(pending.peer_entity_id(), peer_entity_id)?;
575 ensure_logout_response_binding(input_binding(&input), pending.response_binding())?;
576 ensure_relay_state(pending.relay_state(), &relay_state_from_input(&input)?)?;
577 let expected_recipient = logout_recipient_endpoint(local_metadata, pending.response_binding())?;
578 let request = HttpRequest::try_from(input)?;
579 let flow = parse_logout_response_at(
580 local_setting,
581 peer_metadata,
582 pending.response_binding().as_binding(),
583 &request,
584 pending.id().as_str(),
585 LogoutFlowValidation::typed(
586 &expected_recipient,
587 validation.now(),
588 validation.clock_skew().as_millis(),
589 ),
590 )?;
591 let response = LogoutResponse::try_from(flow)?;
592 validation
593 .check_and_store_message_replay(ReplayKey::LogoutResponseId(response.id().clone()))?;
594 Ok(LogoutCompleted::from_response(
595 peer_entity_id.clone(),
596 response,
597 ))
598}
599
600#[derive(Debug, Clone, Copy)]
601enum LogoutResponseTransport {
602 SessionParticipant,
603 SessionAuthority,
604}
605
606fn enforce_participant_https(
607 peer_metadata: &Metadata,
608 binding: Binding,
609 follows_rules: bool,
610 allow_http: bool,
611) -> Result<(), SamlError> {
612 if !follows_rules || allow_http {
613 return Ok(());
614 }
615 let destination = peer_metadata
616 .get_single_logout_service(binding)
617 .ok_or_else(|| Error::MissingMetadata("SingleLogoutService".into()))?;
618 require_https_logout_endpoint(&destination)
619}
620
621fn require_https_logout_endpoint(endpoint: &str) -> Result<(), SamlError> {
622 let url = url::Url::parse(endpoint).map_err(|error| {
623 Error::Invalid(format!(
624 "SingleLogoutService location is not a URL: {error}"
625 ))
626 })?;
627 if url.scheme() == "https" {
628 return Ok(());
629 }
630 Err(Error::ProtocolProfile(
631 "Single Logout sends the user agent to an https SingleLogoutService unless allow_http_single_logout is selected".into(),
632 ))
633}
634
635fn logout_request_signing(setting: &EntitySetting, signing: LogoutSigning) -> bool {
636 match signing {
637 LogoutSigning::FollowLocalPolicy => setting.want_logout_request_signed,
638 LogoutSigning::Sign => true,
639 LogoutSigning::DoNotSignForCompatibility => false,
640 }
641}
642
643fn ensure_logout_response_binding(
644 actual: Binding,
645 expected: LogoutBinding,
646) -> Result<(), SamlError> {
647 if actual == expected.as_binding() {
648 return Ok(());
649 }
650 Err(Error::UnsupportedBinding { binding: actual })
651}
652
653fn logout_recipient_endpoint(
654 local_metadata: &Metadata,
655 binding: LogoutBinding,
656) -> Result<String, SamlError> {
657 local_metadata
658 .get_single_logout_service(binding.as_binding())
659 .ok_or_else(|| Error::MissingMetadata("SingleLogoutService".into()))
660}
661
662fn typed_logout_subject(subject: LogoutSubject) -> TypedLogoutSubject {
663 TypedLogoutSubject {
664 name_id: subject.name_id().value().to_string(),
665 session_indexes: subject
666 .session_indexes()
667 .iter()
668 .map(|session_index| session_index.as_str().to_string())
669 .collect(),
670 }
671}