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::EntitySetting;
5use crate::error::SamlError as Error;
6use crate::flow::HttpRequest;
7use crate::logout::{
8    create_logout_request_with_session_indexes, create_logout_response_checked,
9    parse_logout_request_at, parse_logout_response_at, LogoutRequestSessionIndexes,
10};
11use crate::metadata::Metadata;
12use crate::model::{
13    LogoutCompleted, LogoutRequest, LogoutResponse, LogoutSubject, Received, ReplayKey,
14    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    /// Start SP-initiated Single Logout.
25    ///
26    /// # Errors
27    ///
28    /// Returns [`SamlError`] when relay state is invalid, IdP metadata cannot
29    /// be parsed, a compatible logout endpoint or signing key is missing, the
30    /// selected binding is unsupported, or logout request creation fails.
31    ///
32    /// # Examples
33    ///
34    /// ```no_run
35    /// use saml_rs::{IdpDescriptor, LogoutSubject, Saml, StartSlo};
36    ///
37    /// # fn logout(
38    /// #     sp: &Saml<saml_rs::Sp>,
39    /// #     idp: &IdpDescriptor,
40    /// #     subject: LogoutSubject,
41    /// # ) -> Result<(), saml_rs::SamlError> {
42    /// let started = sp.start_slo(idp, subject, StartSlo::post())?;
43    /// let form = started.outbound.post_form()?;
44    /// let snapshot = started.pending.snapshot();
45    /// # let _ = (form, snapshot);
46    /// # Ok(()) }
47    /// ```
48    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        )
63    }
64
65    /// Receive an IdP LogoutRequest.
66    ///
67    /// # Errors
68    ///
69    /// Returns [`SamlError`] when the browser input or relay state is invalid,
70    /// the binding is unsupported for logout, IdP metadata cannot be parsed,
71    /// XML parsing or signature/trust validation fails, the destination does
72    /// not match local metadata, or replay validation detects a duplicate or
73    /// expired message.
74    pub fn receive_slo(
75        &self,
76        idp: &IdpDescriptor,
77        input: BrowserInput<LogoutRequest>,
78        validation: SamlValidationContext<'_>,
79    ) -> Result<Received<LogoutRequest>, SamlError> {
80        let raw_idp = raw_idp_descriptor(idp)?;
81        receive_slo_impl(
82            &self.raw_service_provider().setting,
83            &self.raw_service_provider().metadata,
84            &raw_idp.metadata,
85            input,
86            validation,
87        )
88    }
89
90    /// Respond to a received IdP LogoutRequest.
91    ///
92    /// # Errors
93    ///
94    /// Returns [`SamlError`] when IdP metadata cannot be parsed, relay state is
95    /// invalid, a compatible logout endpoint or signing key is missing, the
96    /// selected binding is unsupported, or logout response creation fails.
97    pub fn respond_slo(
98        &self,
99        idp: &IdpDescriptor,
100        request: &Received<LogoutRequest>,
101        options: RespondSlo,
102    ) -> Result<Outbound<LogoutResponse>, SamlError> {
103        let raw_idp = raw_idp_descriptor(idp)?;
104        respond_slo_impl(
105            &self.raw_service_provider().setting,
106            &self.raw_service_provider().metadata,
107            &raw_idp.metadata,
108            request,
109            options,
110        )
111    }
112
113    /// Finish SP-initiated Single Logout using stored pending LogoutRequest state.
114    ///
115    /// # Errors
116    ///
117    /// Returns [`SamlError`] when the response does not match the pending
118    /// request, including issuer, binding, relay state, destination, or
119    /// `InResponseTo` mismatches; when IdP metadata cannot be parsed; when XML,
120    /// signature, trust, status, or time validation fails; or when replay
121    /// validation detects a duplicate or expired message.
122    ///
123    /// # Examples
124    ///
125    /// ```no_run
126    /// use saml_rs::{
127    ///     BrowserInput, FormField, IdpDescriptor, LogoutResponse, PendingLogoutRequest,
128    ///     ReplayPolicy, Saml, SamlValidationContext,
129    /// };
130    /// use std::time::SystemTime;
131    ///
132    /// # fn finish(
133    /// #     sp: &Saml<saml_rs::Sp>,
134    /// #     idp: &IdpDescriptor,
135    /// #     pending: &PendingLogoutRequest,
136    /// #     fields: Vec<FormField>,
137    /// # ) -> Result<(), saml_rs::SamlError> {
138    /// let validation = SamlValidationContext::new(
139    ///     SystemTime::now(),
140    ///     ReplayPolicy::DisabledForCompatibility,
141    /// );
142    /// let completed = sp.finish_slo(
143    ///     idp,
144    ///     pending,
145    ///     BrowserInput::<LogoutResponse>::post(fields),
146    ///     validation,
147    /// )?;
148    ///
149    /// let peer = completed.peer_entity_id().as_str();
150    /// # let _ = peer;
151    /// # Ok(()) }
152    /// ```
153    pub fn finish_slo(
154        &self,
155        idp: &IdpDescriptor,
156        pending: &PendingLogoutRequest,
157        input: BrowserInput<LogoutResponse>,
158        validation: SamlValidationContext<'_>,
159    ) -> Result<LogoutCompleted, SamlError> {
160        let raw_idp = raw_idp_descriptor(idp)?;
161        finish_slo_impl(
162            &self.raw_service_provider().setting,
163            &self.raw_service_provider().metadata,
164            idp.entity_id(),
165            &raw_idp.metadata,
166            pending,
167            input,
168            validation,
169        )
170    }
171}
172
173impl Saml<Idp> {
174    /// Start IdP-initiated Single Logout.
175    ///
176    /// # Errors
177    ///
178    /// Returns [`SamlError`] when relay state is invalid, SP metadata cannot be
179    /// parsed, a compatible logout endpoint or signing key is missing, the
180    /// selected binding is unsupported, or logout request creation fails.
181    ///
182    /// # Examples
183    ///
184    /// ```no_run
185    /// use saml_rs::{LogoutSubject, Saml, SpDescriptor, StartSlo};
186    ///
187    /// # fn logout(
188    /// #     idp: &Saml<saml_rs::Idp>,
189    /// #     sp: &SpDescriptor,
190    /// #     subject: LogoutSubject,
191    /// # ) -> Result<(), saml_rs::SamlError> {
192    /// let started = idp.start_slo(sp, subject, StartSlo::post())?;
193    /// let form = started.outbound.post_form()?;
194    /// let snapshot = started.pending.snapshot();
195    /// # let _ = (form, snapshot);
196    /// # Ok(()) }
197    /// ```
198    pub fn start_slo(
199        &self,
200        sp: &SpDescriptor,
201        subject: LogoutSubject,
202        options: StartSlo,
203    ) -> Result<Started<LogoutRequest>, SamlError> {
204        let raw_sp = raw_sp_descriptor(sp)?;
205        start_slo_impl(
206            &self.raw_identity_provider().setting,
207            &self.raw_identity_provider().metadata,
208            sp.entity_id(),
209            &raw_sp.metadata,
210            subject,
211            options,
212        )
213    }
214
215    /// Receive an SP LogoutRequest.
216    ///
217    /// # Errors
218    ///
219    /// Returns [`SamlError`] when the browser input or relay state is invalid,
220    /// the binding is unsupported for logout, SP metadata cannot be parsed, XML
221    /// parsing or signature/trust validation fails, the destination does not
222    /// match local metadata, or replay validation detects a duplicate or
223    /// expired message.
224    ///
225    /// # Examples
226    ///
227    /// ```no_run
228    /// use saml_rs::{
229    ///     BrowserInput, FormField, LogoutRequest, ReplayPolicy, RespondSlo, Saml,
230    ///     SamlValidationContext, SpDescriptor,
231    /// };
232    /// use std::time::SystemTime;
233    ///
234    /// # fn respond(
235    /// #     idp: &Saml<saml_rs::Idp>,
236    /// #     sp: &SpDescriptor,
237    /// #     fields: Vec<FormField>,
238    /// # ) -> Result<(), saml_rs::SamlError> {
239    /// let validation = SamlValidationContext::new(
240    ///     SystemTime::now(),
241    ///     ReplayPolicy::DisabledForCompatibility,
242    /// );
243    /// let input = BrowserInput::<LogoutRequest>::post(fields);
244    /// let request = idp.receive_slo(sp, input, validation)?;
245    /// let response = idp.respond_slo(sp, &request, RespondSlo::post())?;
246    ///
247    /// let form = response.post_form()?;
248    /// # let _ = form;
249    /// # Ok(()) }
250    /// ```
251    pub fn receive_slo(
252        &self,
253        sp: &SpDescriptor,
254        input: BrowserInput<LogoutRequest>,
255        validation: SamlValidationContext<'_>,
256    ) -> Result<Received<LogoutRequest>, SamlError> {
257        let raw_sp = raw_sp_descriptor(sp)?;
258        receive_slo_impl(
259            &self.raw_identity_provider().setting,
260            &self.raw_identity_provider().metadata,
261            &raw_sp.metadata,
262            input,
263            validation,
264        )
265    }
266
267    /// Respond to a received SP LogoutRequest.
268    ///
269    /// # Errors
270    ///
271    /// Returns [`SamlError`] when SP metadata cannot be parsed, relay state is
272    /// invalid, a compatible logout endpoint or signing key is missing, the
273    /// selected binding is unsupported, or logout response creation fails.
274    ///
275    /// # Examples
276    ///
277    /// ```no_run
278    /// use saml_rs::{LogoutRequest, Received, RespondSlo, Saml, SpDescriptor};
279    ///
280    /// # fn respond(
281    /// #     idp: &Saml<saml_rs::Idp>,
282    /// #     sp: &SpDescriptor,
283    /// #     request: &Received<LogoutRequest>,
284    /// # ) -> Result<(), saml_rs::SamlError> {
285    /// let response = idp.respond_slo(sp, request, RespondSlo::post())?;
286    /// let form = response.post_form()?;
287    /// # let _ = form;
288    /// # Ok(()) }
289    /// ```
290    pub fn respond_slo(
291        &self,
292        sp: &SpDescriptor,
293        request: &Received<LogoutRequest>,
294        options: RespondSlo,
295    ) -> Result<Outbound<LogoutResponse>, SamlError> {
296        let raw_sp = raw_sp_descriptor(sp)?;
297        respond_slo_impl(
298            &self.raw_identity_provider().setting,
299            &self.raw_identity_provider().metadata,
300            &raw_sp.metadata,
301            request,
302            options,
303        )
304    }
305
306    /// Finish IdP-initiated Single Logout using stored pending LogoutRequest state.
307    ///
308    /// # Errors
309    ///
310    /// Returns [`SamlError`] when the response does not match the pending
311    /// request, including issuer, binding, relay state, destination, or
312    /// `InResponseTo` mismatches; when SP metadata cannot be parsed; when XML,
313    /// signature, trust, status, or time validation fails; or when replay
314    /// validation detects a duplicate or expired message.
315    ///
316    /// # Examples
317    ///
318    /// ```no_run
319    /// use saml_rs::{
320    ///     BrowserInput, FormField, LogoutResponse, PendingLogoutRequest, ReplayPolicy,
321    ///     Saml, SamlValidationContext, SpDescriptor,
322    /// };
323    /// use std::time::SystemTime;
324    ///
325    /// # fn finish(
326    /// #     idp: &Saml<saml_rs::Idp>,
327    /// #     sp: &SpDescriptor,
328    /// #     pending: &PendingLogoutRequest,
329    /// #     fields: Vec<FormField>,
330    /// # ) -> Result<(), saml_rs::SamlError> {
331    /// let validation = SamlValidationContext::new(
332    ///     SystemTime::now(),
333    ///     ReplayPolicy::DisabledForCompatibility,
334    /// );
335    /// let completed = idp.finish_slo(
336    ///     sp,
337    ///     pending,
338    ///     BrowserInput::<LogoutResponse>::post(fields),
339    ///     validation,
340    /// )?;
341    ///
342    /// let peer = completed.peer_entity_id().as_str();
343    /// # let _ = peer;
344    /// # Ok(()) }
345    /// ```
346    pub fn finish_slo(
347        &self,
348        sp: &SpDescriptor,
349        pending: &PendingLogoutRequest,
350        input: BrowserInput<LogoutResponse>,
351        validation: SamlValidationContext<'_>,
352    ) -> Result<LogoutCompleted, SamlError> {
353        let raw_sp = raw_sp_descriptor(sp)?;
354        finish_slo_impl(
355            &self.raw_identity_provider().setting,
356            &self.raw_identity_provider().metadata,
357            sp.entity_id(),
358            &raw_sp.metadata,
359            pending,
360            input,
361            validation,
362        )
363    }
364}
365
366struct TypedLogoutSubject {
367    name_id: String,
368    session_indexes: Vec<String>,
369}
370
371fn start_slo_impl(
372    local_setting: &EntitySetting,
373    local_metadata: &Metadata,
374    peer_entity_id: &EntityId,
375    peer_metadata: &Metadata,
376    subject: LogoutSubject,
377    options: StartSlo,
378) -> Result<Started<LogoutRequest>, SamlError> {
379    options.relay_state.validate()?;
380    let subject = typed_logout_subject(subject);
381    let context = create_logout_request_with_session_indexes(LogoutRequestSessionIndexes {
382        init_setting: local_setting,
383        init_meta: local_metadata,
384        target_meta: peer_metadata,
385        binding: options.binding.as_binding(),
386        name_id: &subject.name_id,
387        session_indexes: &subject.session_indexes,
388        relay_state: options.relay_state.as_deref(),
389        want_signed: logout_request_signing(local_setting, options.signing),
390    })?;
391    let outbound = Outbound::<LogoutRequest>::try_from(context)?;
392    let pending = PendingLogoutRequest::try_new(
393        outbound.id().clone(),
394        options.relay_state,
395        options.binding,
396        peer_entity_id.clone(),
397    )?;
398    Ok(Started { pending, outbound })
399}
400
401fn receive_slo_impl(
402    local_setting: &EntitySetting,
403    local_metadata: &Metadata,
404    peer_metadata: &Metadata,
405    input: BrowserInput<LogoutRequest>,
406    mut validation: SamlValidationContext<'_>,
407) -> Result<Received<LogoutRequest>, SamlError> {
408    let relay_state = relay_state_from_input(&input)?;
409    let binding = LogoutBinding::try_from(input_binding(&input))?;
410    let request = HttpRequest::try_from(input)?;
411    let flow = parse_logout_request_at(
412        local_setting,
413        peer_metadata,
414        binding.as_binding(),
415        &request,
416        validation.now(),
417        validation.clock_skew().as_millis(),
418    )?;
419    let logout = LogoutRequest::try_from(flow)?;
420    ensure_logout_destination(local_metadata, binding, logout.destination())?;
421    validation.check_and_store_message_replay(ReplayKey::LogoutRequestId(logout.id().clone()))?;
422    Ok(Received::new(logout).with_relay_state(relay_state))
423}
424
425fn respond_slo_impl(
426    local_setting: &EntitySetting,
427    local_metadata: &Metadata,
428    peer_metadata: &Metadata,
429    request: &Received<LogoutRequest>,
430    options: RespondSlo,
431) -> Result<Outbound<LogoutResponse>, SamlError> {
432    let relay_state = options
433        .relay_state
434        .unwrap_or_else(|| request.relay_state().clone());
435    relay_state.validate()?;
436    let context = create_logout_response_checked(
437        local_setting,
438        local_metadata,
439        peer_metadata,
440        options.binding.as_binding(),
441        Some(request.message().id().as_str()),
442        relay_state.as_deref(),
443        logout_response_signing(local_setting, options.signing),
444    )?;
445    Outbound::<LogoutResponse>::try_from(context)
446}
447
448fn finish_slo_impl(
449    local_setting: &EntitySetting,
450    local_metadata: &Metadata,
451    peer_entity_id: &EntityId,
452    peer_metadata: &Metadata,
453    pending: &PendingLogoutRequest,
454    input: BrowserInput<LogoutResponse>,
455    mut validation: SamlValidationContext<'_>,
456) -> Result<LogoutCompleted, SamlError> {
457    ensure_entity_id(pending.peer_entity_id(), peer_entity_id)?;
458    ensure_logout_response_binding(input_binding(&input), pending.response_binding())?;
459    ensure_relay_state(pending.relay_state(), &relay_state_from_input(&input)?)?;
460    let request = HttpRequest::try_from(input)?;
461    let flow = parse_logout_response_at(
462        local_setting,
463        peer_metadata,
464        pending.response_binding().as_binding(),
465        &request,
466        pending.id().as_str(),
467        validation.now(),
468        validation.clock_skew().as_millis(),
469    )?;
470    let response = LogoutResponse::try_from(flow)?;
471    ensure_logout_destination(
472        local_metadata,
473        pending.response_binding(),
474        response.destination(),
475    )?;
476    validation
477        .check_and_store_message_replay(ReplayKey::LogoutResponseId(response.id().clone()))?;
478    Ok(LogoutCompleted::from_response(
479        peer_entity_id.clone(),
480        response,
481    ))
482}
483
484fn logout_request_signing(setting: &EntitySetting, signing: LogoutSigning) -> bool {
485    match signing {
486        LogoutSigning::FollowLocalPolicy => setting.want_logout_request_signed,
487        LogoutSigning::Sign => true,
488        LogoutSigning::DoNotSignForCompatibility => false,
489    }
490}
491
492fn logout_response_signing(setting: &EntitySetting, signing: LogoutSigning) -> bool {
493    match signing {
494        LogoutSigning::FollowLocalPolicy => setting.want_logout_response_signed,
495        LogoutSigning::Sign => true,
496        LogoutSigning::DoNotSignForCompatibility => false,
497    }
498}
499
500fn ensure_logout_response_binding(
501    actual: Binding,
502    expected: LogoutBinding,
503) -> Result<(), SamlError> {
504    if actual == expected.as_binding() {
505        return Ok(());
506    }
507    Err(Error::UnsupportedBinding { binding: actual })
508}
509
510fn ensure_logout_destination(
511    local_metadata: &Metadata,
512    binding: LogoutBinding,
513    actual: Option<&crate::model::EndpointUrl>,
514) -> Result<(), SamlError> {
515    let Some(actual) = actual else {
516        return Ok(());
517    };
518    let expected = local_metadata
519        .get_single_logout_service(binding.as_binding())
520        .ok_or_else(|| Error::MissingMetadata("SingleLogoutService".into()))?;
521    if actual.as_str() == expected {
522        return Ok(());
523    }
524    Err(Error::destination_mismatch(
525        &expected,
526        Some(actual.as_str()),
527    ))
528}
529
530fn typed_logout_subject(subject: LogoutSubject) -> TypedLogoutSubject {
531    TypedLogoutSubject {
532        name_id: subject.name_id().value().to_string(),
533        session_indexes: subject
534            .session_indexes()
535            .iter()
536            .map(|session_index| session_index.as_str().to_string())
537            .collect(),
538    }
539}