Skip to main content

saml_rs/api/
slo.rs

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    /// Start SP-initiated Single Logout.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`SamlError`] when relay state is invalid, IdP metadata cannot
30    /// be parsed, a compatible logout endpoint or signing key is missing, the
31    /// selected binding is unsupported, or logout request creation fails.
32    /// [`StartSlo::apply_single_logout_generation_rules`] also rejects a
33    /// missing `SessionIndex`, an `http` peer endpoint unless
34    /// [`StartSlo::allow_http_single_logout`] is selected, and rejects the
35    /// samlify-port [`LogoutSigning::DoNotSignForCompatibility`].
36    ///
37    /// # Examples
38    ///
39    /// ```no_run
40    /// use saml_rs::{IdpDescriptor, LogoutSubject, Saml, StartSlo};
41    ///
42    /// # fn logout(
43    /// #     sp: &Saml<saml_rs::Sp>,
44    /// #     idp: &IdpDescriptor,
45    /// #     subject: LogoutSubject,
46    /// # ) -> Result<(), saml_rs::SamlError> {
47    /// let started = sp.start_slo(idp, subject, StartSlo::post())?;
48    /// let form = started.outbound.post_form()?;
49    /// let snapshot = started.pending.snapshot();
50    /// # let _ = (form, snapshot);
51    /// # Ok(()) }
52    /// ```
53    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    /// Receive an IdP LogoutRequest.
72    ///
73    /// # Errors
74    ///
75    /// Returns [`SamlError`] when the browser input or relay state is invalid,
76    /// the binding is unsupported for logout, IdP metadata cannot be parsed,
77    /// XML parsing or signature/trust validation fails, required
78    /// `IssueInstant` or optional `NotOnOrAfter` is not conformant,
79    /// `NotOnOrAfter` has expired under saml-rs' fail-closed policy, the
80    /// destination does not match local metadata, or replay validation detects
81    /// a duplicate or unusable expiration.
82    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    /// Respond to a received IdP LogoutRequest.
99    ///
100    /// # Errors
101    ///
102    /// Returns [`SamlError`] when IdP metadata cannot be parsed, relay state is
103    /// invalid, a compatible logout endpoint or signing key is missing, the
104    /// selected binding is unsupported, or logout response creation fails.
105    /// [`RespondSlo::apply_single_logout_generation_rules`] also rejects an
106    /// `http` peer endpoint unless
107    /// [`RespondSlo::allow_http_single_logout`] is
108    /// selected.
109    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    /// Finish SP-initiated Single Logout using stored pending LogoutRequest state.
127    ///
128    /// # Errors
129    ///
130    /// Returns [`SamlError`] when the response does not match the pending
131    /// request, including issuer, binding, relay state, destination, or
132    /// `InResponseTo` mismatches; when IdP metadata cannot be parsed; when XML,
133    /// signature, trust, status, or time validation fails; or when replay
134    /// validation detects a duplicate or expired message. Response signatures
135    /// follow [`crate::LogoutPolicy::responses`]. The Single Logout accept
136    /// combination sets that field to [`crate::LogoutSignaturePolicy::RequireSigned`].
137    ///
138    /// # Examples
139    ///
140    /// ```no_run
141    /// use saml_rs::{
142    ///     BrowserInput, FormField, IdpDescriptor, LogoutResponse, PendingLogoutRequest,
143    ///     ReplayPolicy, Saml, SamlValidationContext,
144    /// };
145    /// use std::time::SystemTime;
146    ///
147    /// # fn finish(
148    /// #     sp: &Saml<saml_rs::Sp>,
149    /// #     idp: &IdpDescriptor,
150    /// #     pending: &PendingLogoutRequest,
151    /// #     fields: Vec<FormField>,
152    /// # ) -> Result<(), saml_rs::SamlError> {
153    /// let validation = SamlValidationContext::new(
154    ///     SystemTime::now(),
155    ///     ReplayPolicy::DisabledForCompatibility,
156    /// );
157    /// let completed = sp.finish_slo(
158    ///     idp,
159    ///     pending,
160    ///     BrowserInput::<LogoutResponse>::post(fields),
161    ///     validation,
162    /// )?;
163    ///
164    /// let peer = completed.peer_entity_id().as_str();
165    /// # let _ = peer;
166    /// # Ok(()) }
167    /// ```
168    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    /// Start Session Authority Single Logout.
190    ///
191    /// The generated `LogoutRequest` always carries a UTC `NotOnOrAfter`
192    /// derived from the configured [`crate::IdpConfig::issuance_lifetime`] and
193    /// the same captured `IssueInstant`. That attribute cannot be omitted.
194    /// [`StartSlo::apply_single_logout_generation_rules`] signs the request
195    /// and still allows `SessionIndex` to be omitted. It does not require an
196    /// `https` peer endpoint.
197    ///
198    /// # Errors
199    ///
200    /// Returns [`SamlError`] when relay state is invalid, SP metadata cannot be
201    /// parsed, a compatible logout endpoint or signing key is missing, the
202    /// selected binding is unsupported, logout request creation fails, or the
203    /// configured issuance lifetime cannot be added to the current issue
204    /// instant. [`StartSlo::apply_single_logout_generation_rules`] also
205    /// rejects [`LogoutSigning::DoNotSignForCompatibility`].
206    ///
207    /// # Examples
208    ///
209    /// ```no_run
210    /// use saml_rs::{LogoutSubject, Saml, SpDescriptor, StartSlo};
211    ///
212    /// # fn logout(
213    /// #     idp: &Saml<saml_rs::Idp>,
214    /// #     sp: &SpDescriptor,
215    /// #     subject: LogoutSubject,
216    /// # ) -> Result<(), saml_rs::SamlError> {
217    /// let started = idp.start_slo(sp, subject, StartSlo::post())?;
218    /// let form = started.outbound.post_form()?;
219    /// let snapshot = started.pending.snapshot();
220    /// # let _ = (form, snapshot);
221    /// # Ok(()) }
222    /// ```
223    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    /// Receive an SP LogoutRequest.
244    ///
245    /// # Errors
246    ///
247    /// Returns [`SamlError`] when the browser input or relay state is invalid,
248    /// the binding is unsupported for logout, SP metadata cannot be parsed, XML
249    /// parsing or signature/trust validation fails, required `IssueInstant` or
250    /// optional `NotOnOrAfter` is not conformant, `NotOnOrAfter` has expired
251    /// under saml-rs' fail-closed policy, the destination does not match local
252    /// metadata, or replay validation detects a duplicate or unusable
253    /// expiration.
254    ///
255    /// # Examples
256    ///
257    /// ```no_run
258    /// use saml_rs::{
259    ///     BrowserInput, FormField, LogoutRequest, ReplayPolicy, RespondSlo, Saml,
260    ///     SamlValidationContext, SpDescriptor,
261    /// };
262    /// use std::time::SystemTime;
263    ///
264    /// # fn respond(
265    /// #     idp: &Saml<saml_rs::Idp>,
266    /// #     sp: &SpDescriptor,
267    /// #     fields: Vec<FormField>,
268    /// # ) -> Result<(), saml_rs::SamlError> {
269    /// let validation = SamlValidationContext::new(
270    ///     SystemTime::now(),
271    ///     ReplayPolicy::DisabledForCompatibility,
272    /// );
273    /// let input = BrowserInput::<LogoutRequest>::post(fields);
274    /// let request = idp.receive_slo(sp, input, validation)?;
275    /// let response = idp.respond_slo(sp, &request, RespondSlo::post())?;
276    ///
277    /// let form = response.post_form()?;
278    /// # let _ = form;
279    /// # Ok(()) }
280    /// ```
281    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    /// Respond to a received SP LogoutRequest.
298    ///
299    /// # Errors
300    ///
301    /// Returns [`SamlError`] when SP metadata cannot be parsed, relay state is
302    /// invalid, a compatible logout endpoint or signing key is missing, the
303    /// selected binding is unsupported, or logout response creation fails.
304    /// [`RespondSlo::apply_single_logout_generation_rules`] does not add an
305    /// `https` requirement for this role.
306    ///
307    /// # Examples
308    ///
309    /// ```no_run
310    /// use saml_rs::{LogoutRequest, Received, RespondSlo, Saml, SpDescriptor};
311    ///
312    /// # fn respond(
313    /// #     idp: &Saml<saml_rs::Idp>,
314    /// #     sp: &SpDescriptor,
315    /// #     request: &Received<LogoutRequest>,
316    /// # ) -> Result<(), saml_rs::SamlError> {
317    /// let response = idp.respond_slo(sp, request, RespondSlo::post())?;
318    /// let form = response.post_form()?;
319    /// # let _ = form;
320    /// # Ok(()) }
321    /// ```
322    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    /// Finish IdP-initiated Single Logout using stored pending LogoutRequest state.
340    ///
341    /// # Errors
342    ///
343    /// Returns [`SamlError`] when the response does not match the pending
344    /// request, including issuer, binding, relay state, destination, or
345    /// `InResponseTo` mismatches; when SP metadata cannot be parsed; when XML,
346    /// signature, trust, status, or time validation fails; or when replay
347    /// validation detects a duplicate or expired message. Response signatures
348    /// follow [`crate::LogoutPolicy::responses`]. The Single Logout accept
349    /// combination sets that field to [`crate::LogoutSignaturePolicy::RequireSigned`].
350    ///
351    /// # Examples
352    ///
353    /// ```no_run
354    /// use saml_rs::{
355    ///     BrowserInput, FormField, LogoutResponse, PendingLogoutRequest, ReplayPolicy,
356    ///     Saml, SamlValidationContext, SpDescriptor,
357    /// };
358    /// use std::time::SystemTime;
359    ///
360    /// # fn finish(
361    /// #     idp: &Saml<saml_rs::Idp>,
362    /// #     sp: &SpDescriptor,
363    /// #     pending: &PendingLogoutRequest,
364    /// #     fields: Vec<FormField>,
365    /// # ) -> Result<(), saml_rs::SamlError> {
366    /// let validation = SamlValidationContext::new(
367    ///     SystemTime::now(),
368    ///     ReplayPolicy::DisabledForCompatibility,
369    /// );
370    /// let completed = idp.finish_slo(
371    ///     sp,
372    ///     pending,
373    ///     BrowserInput::<LogoutResponse>::post(fields),
374    ///     validation,
375    /// )?;
376    ///
377    /// let peer = completed.peer_entity_id().as_str();
378    /// # let _ = peer;
379    /// # Ok(()) }
380    /// ```
381    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        // SAML Profiles 2.0 ยงยง4.4.3.4 and 4.4.4.2 require front-channel
557        // LogoutResponses to authenticate the responder and protect integrity.
558        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}