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 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 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 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 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 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 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 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 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}