1use crate::binding::{
5 base64_decode_with_limit, deflate_raw_decode_with_limit, MAX_DEFLATE_RAW_DECODE_BYTES,
6};
7use crate::constants::{Binding, ParserType};
8use crate::context::is_valid_xml_with_limits;
9#[cfg(feature = "crypto-bergshamra")]
10use crate::error::SignatureVerificationReason;
11use crate::error::{SamlError, SubjectConfirmationReason, TimeWindowField};
12#[cfg(feature = "crypto-bergshamra")]
13use crate::model::RelayStateParam;
14use crate::model::{authn_statement_not_on_or_after_values, earliest_authn_session_expiration};
15use crate::util::Value;
16use crate::validator::{check_status_with_limits, conditions_time_bounds, verify_time_at};
17use crate::xml::{
18 extract_with_limits, fields, validate_protocol_profile, ExtractorField, XmlLimits,
19};
20use std::time::SystemTime;
21use time::{Duration, OffsetDateTime};
22
23const BEARER_SUBJECT_CONFIRMATION_METHOD: &str = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
24
25#[derive(Debug, Default, Clone)]
27pub struct HttpRequest {
28 pub query: Vec<(String, String)>,
30 pub body: Vec<(String, String)>,
32 pub octet_string: Option<String>,
34}
35
36impl HttpRequest {
37 pub fn redirect(query: Vec<(String, String)>) -> Self {
39 Self {
40 query,
41 ..Default::default()
42 }
43 }
44
45 pub fn post(body: Vec<(String, String)>) -> Self {
47 Self {
48 body,
49 ..Default::default()
50 }
51 }
52
53 fn query_get(&self, key: &str) -> Result<Option<&str>, SamlError> {
54 single_param(&self.query, key)
55 }
56
57 fn body_get(&self, key: &str) -> Result<Option<&str>, SamlError> {
58 single_param(&self.body, key)
59 }
60}
61
62fn single_param<'a>(
63 params: &'a [(String, String)],
64 key: &str,
65) -> Result<Option<&'a str>, SamlError> {
66 let mut values = params
67 .iter()
68 .filter(|(candidate, _)| candidate == key)
69 .map(|(_, value)| value.as_str());
70 let first = values.next();
71 if values.next().is_some() {
72 return Err(SamlError::Invalid("ERR_AMBIGUOUS_FLOW_INPUT".into()));
73 }
74 Ok(first)
75}
76
77fn missing_binding_parameter(name: &'static str) -> SamlError {
78 SamlError::MissingBindingParameter { name }
79}
80
81fn unsupported_binding(binding: Binding) -> SamlError {
82 SamlError::UnsupportedBinding { binding }
83}
84
85#[non_exhaustive]
87#[derive(Debug, Clone)]
88pub struct FlowOptions<'a> {
89 pub binding: Option<Binding>,
91 pub parser_type: Option<ParserType>,
93 pub redirect_inflate_max_bytes: usize,
96 pub xml_limits: XmlLimits,
98 pub check_signature: bool,
100 pub from_issuer: Option<&'a str>,
102 pub signing_certs: &'a [String],
104 pub decrypt_key: Option<&'a str>,
106 pub decrypt_key_pass: Option<&'a str>,
108 pub allow_insecure_software_rsa_key_transport_decryption: bool,
113 pub clock_drifts: (i64, i64),
115 pub now: Option<SystemTime>,
118 pub expected_audience: Option<&'a str>,
120 pub expected_in_response_to: Option<&'a str>,
122}
123
124impl<'a> Default for FlowOptions<'a> {
125 fn default() -> Self {
126 Self {
127 binding: None,
128 parser_type: None,
129 redirect_inflate_max_bytes: MAX_DEFLATE_RAW_DECODE_BYTES,
130 xml_limits: XmlLimits::default(),
131 check_signature: false,
132 from_issuer: None,
133 signing_certs: &[],
134 decrypt_key: None,
135 decrypt_key_pass: None,
136 allow_insecure_software_rsa_key_transport_decryption: false,
137 clock_drifts: (0, 0),
138 now: None,
139 expected_audience: None,
140 expected_in_response_to: None,
141 }
142 }
143}
144
145impl FlowOptions<'_> {
146 fn validation_now(&self) -> Result<OffsetDateTime, SamlError> {
147 self.now.map_or_else(
148 || Ok(OffsetDateTime::now_utc()),
149 crate::validator::offset_datetime_from_system_time,
150 )
151 }
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub(crate) enum AssertionSignatureRequirement {
156 Compatible,
157 Direct,
158}
159
160#[derive(Debug, Clone)]
162pub struct FlowResult {
163 pub saml_content: String,
165 pub extract: Value,
167 pub sig_alg: Option<String>,
169}
170
171fn default_fields(
172 parser_type: ParserType,
173 assertion: Option<&str>,
174) -> Result<Vec<ExtractorField>, SamlError> {
175 Ok(match parser_type {
176 ParserType::SamlRequest => fields::login_request_fields(),
177 ParserType::SamlResponse => {
178 let assertion =
179 assertion.ok_or_else(|| SamlError::Xml("ERR_EMPTY_ASSERTION".into()))?;
180 fields::login_response_fields(assertion)
181 }
182 ParserType::LogoutRequest => fields::logout_request_fields(),
183 ParserType::LogoutResponse => fields::logout_response_fields(),
184 })
185}
186
187fn decode_message(
188 binding: Binding,
189 parser_type: ParserType,
190 request: &HttpRequest,
191 redirect_inflate_max_bytes: usize,
192 xml_limits: XmlLimits,
193) -> Result<String, SamlError> {
194 let direction = parser_type.query_param();
195 let bytes = match binding {
196 Binding::Redirect => {
197 let content = request
198 .query_get(direction)?
199 .ok_or_else(|| missing_binding_parameter(direction))?;
200 let redirect_max_bytes = redirect_inflate_max_bytes.min(xml_limits.max_bytes);
201 let compressed = base64_decode_with_limit(content, redirect_max_bytes)?;
202 deflate_raw_decode_with_limit(&compressed, redirect_max_bytes)?
203 }
204 Binding::Post | Binding::SimpleSign => {
205 let content = request
206 .body_get(direction)?
207 .ok_or_else(|| missing_binding_parameter(direction))?;
208 base64_decode_with_limit(content, xml_limits.max_bytes)?
209 }
210 Binding::Artifact => return Err(unsupported_binding(binding)),
211 };
212 xml_limits.check_input_bytes(bytes.len())?;
213 String::from_utf8(bytes).map_err(|e| SamlError::Xml(e.to_string()))
214}
215
216fn assertion_shortcut(xml: &str, limits: XmlLimits) -> Result<Option<String>, SamlError> {
217 let field = ExtractorField::new("assertion", &["Response", "Assertion"]).with_context();
218 Ok(
219 extract_with_limits(xml, std::slice::from_ref(&field), limits)?
220 .get_str("assertion")
221 .map(str::to_string),
222 )
223}
224
225#[cfg(feature = "crypto-bergshamra")]
226fn verified_content_not_covered() -> SamlError {
227 SamlError::SignedReferenceMismatch
228}
229
230#[cfg(feature = "crypto-bergshamra")]
231fn decoded_octet_params(octet: &str) -> Vec<(String, String)> {
232 url::form_urlencoded::parse(octet.as_bytes())
233 .map(|(key, value)| (key.into_owned(), value.into_owned()))
234 .collect()
235}
236
237#[cfg(feature = "crypto-bergshamra")]
238fn detached_signature_verification() -> SamlError {
239 SamlError::SignatureVerification {
240 reason: SignatureVerificationReason::DetachedMessageSignature,
241 }
242}
243
244#[cfg(feature = "crypto-bergshamra")]
245fn relay_state_param(value: Option<&str>) -> Option<RelayStateParam> {
246 RelayStateParam::try_from_option(value.map(str::to_string)).ok()
247}
248
249#[cfg(feature = "crypto-bergshamra")]
250fn detached_relay_state_mismatch(expected: Option<&str>, actual: Option<&str>) -> SamlError {
251 match (relay_state_param(expected), relay_state_param(actual)) {
252 (Some(expected), Some(actual)) => SamlError::RelayStateMismatch { expected, actual },
253 _ => SamlError::SignatureVerification {
254 reason: SignatureVerificationReason::RelayStateCorrelation,
255 },
256 }
257}
258
259#[cfg(feature = "crypto-bergshamra")]
260fn ensure_redirect_octet_matches_consumed_fields(
261 parser_type: ParserType,
262 request: &HttpRequest,
263 sig_alg: &str,
264 octet: &str,
265) -> Result<(), SamlError> {
266 let direction = parser_type.query_param();
267 let signed = decoded_octet_params(octet);
268 if single_param(&signed, "Signature")?.is_some() {
269 return Err(detached_signature_verification());
270 }
271
272 let signed_message =
273 single_param(&signed, direction)?.ok_or_else(|| missing_binding_parameter(direction))?;
274 let consumed_message = request
275 .query_get(direction)?
276 .ok_or_else(|| missing_binding_parameter(direction))?;
277 if signed_message != consumed_message {
278 return Err(detached_signature_verification());
279 }
280
281 let signed_sig_alg =
282 single_param(&signed, "SigAlg")?.ok_or_else(|| missing_binding_parameter("SigAlg"))?;
283 if signed_sig_alg != sig_alg {
284 return Err(detached_signature_verification());
285 }
286
287 let signed_relay_state = single_param(&signed, "RelayState")?;
288 let consumed_relay_state = request.query_get("RelayState")?;
289 if signed_relay_state != consumed_relay_state {
290 return Err(detached_relay_state_mismatch(
291 signed_relay_state,
292 consumed_relay_state,
293 ));
294 }
295
296 Ok(())
297}
298
299#[cfg(feature = "crypto-bergshamra")]
300fn ensure_simplesign_octet_matches_consumed_fields(
301 parser_type: ParserType,
302 request: &HttpRequest,
303 xml: &str,
304 sig_alg: &str,
305 octet: &str,
306) -> Result<(), SamlError> {
307 let direction = parser_type.query_param();
308 request
309 .body_get(direction)?
310 .ok_or_else(|| missing_binding_parameter(direction))?;
311
312 let message_and_sig_alg = format!("{direction}={xml}&SigAlg={sig_alg}");
313 let message_empty_relay_and_sig_alg = format!("{direction}={xml}&RelayState=&SigAlg={sig_alg}");
314 let matches = match request.body_get("RelayState")? {
315 Some(relay_state) => {
316 let expected = format!("{direction}={xml}&RelayState={relay_state}&SigAlg={sig_alg}");
317 octet == expected
318 }
319 None => octet == message_and_sig_alg || octet == message_empty_relay_and_sig_alg,
323 };
324
325 if matches {
326 Ok(())
327 } else {
328 Err(detached_signature_verification())
329 }
330}
331
332#[cfg(feature = "crypto-bergshamra")]
333fn ensure_detached_octet_matches_consumed_fields(
334 binding: Binding,
335 parser_type: ParserType,
336 request: &HttpRequest,
337 xml: &str,
338 sig_alg: &str,
339 octet: &str,
340) -> Result<(), SamlError> {
341 match binding {
342 Binding::Redirect => {
343 ensure_redirect_octet_matches_consumed_fields(parser_type, request, sig_alg, octet)
344 }
345 Binding::SimpleSign => ensure_simplesign_octet_matches_consumed_fields(
346 parser_type,
347 request,
348 xml,
349 sig_alg,
350 octet,
351 ),
352 Binding::Post | Binding::Artifact => Ok(()),
353 }
354}
355
356#[cfg(feature = "crypto-bergshamra")]
357fn required_xml_signature_failed(signature_present: bool) -> SamlError {
358 if signature_present {
359 SamlError::SignatureVerification {
360 reason: SignatureVerificationReason::XmlSignature,
361 }
362 } else {
363 SamlError::SignatureMissing
364 }
365}
366
367#[cfg(feature = "crypto-bergshamra")]
368fn verify_embedded_signature(
369 xml: &str,
370 opts: &FlowOptions<'_>,
371 assertion_signature: AssertionSignatureRequirement,
372) -> Result<(bool, Option<String>, bool), SamlError> {
373 use crate::crypto::verify::{
374 verify_signature_with_limits, verify_signatures_detailed_with_limits,
375 };
376
377 match assertion_signature {
378 AssertionSignatureRequirement::Compatible => {
379 let (verified, signed_content) =
380 verify_signature_with_limits(xml, opts.signing_certs, opts.xml_limits)?;
381 Ok((verified, signed_content, false))
382 }
383 AssertionSignatureRequirement::Direct => {
384 let verification =
385 verify_signatures_detailed_with_limits(xml, opts.signing_certs, opts.xml_limits)?;
386 let verified = verification.verified();
387 let assertion_directly_covered = verification.assertion_directly_covered();
388 Ok((
389 verified,
390 verification.into_signed_content(),
391 assertion_directly_covered,
392 ))
393 }
394 }
395}
396
397#[cfg(feature = "crypto-bergshamra")]
398fn require_direct_assertion_coverage(
399 assertion_signature: AssertionSignatureRequirement,
400 assertion_directly_covered: bool,
401) -> Result<(), SamlError> {
402 if assertion_signature == AssertionSignatureRequirement::Direct && !assertion_directly_covered {
403 return Err(SamlError::AssertionSignatureRequired);
404 }
405 Ok(())
406}
407
408#[cfg(feature = "crypto-bergshamra")]
411fn verify_and_prepare(
412 xml: &str,
413 parser_type: ParserType,
414 opts: &FlowOptions<'_>,
415 assertion_signature: AssertionSignatureRequirement,
416) -> Result<(String, Option<String>), SamlError> {
417 use crate::crypto::{
418 decrypt_assertion_with_limits,
419 enc::{software_rsa_decryption_disabled, AssertionDecryptionOptions},
420 keys::load_private_key,
421 verify::has_xml_signature_with_limits,
422 };
423
424 let signature_present = has_xml_signature_with_limits(xml, opts.xml_limits)?;
425 let (verified, verified_node, assertion_directly_covered) =
426 verify_embedded_signature(xml, opts, assertion_signature)?;
427 let decrypt_required = opts.decrypt_key.is_some();
428 if decrypt_required && !opts.allow_insecure_software_rsa_key_transport_decryption {
429 return Err(software_rsa_decryption_disabled());
430 }
431 let decrypt_options = AssertionDecryptionOptions {
432 allow_insecure_software_rsa_key_transport_decryption: opts
433 .allow_insecure_software_rsa_key_transport_decryption,
434 };
435 let load_key = || load_private_key(opts.decrypt_key.unwrap_or_default(), opts.decrypt_key_pass);
436
437 if decrypt_required && verified && parser_type == ParserType::SamlResponse {
438 if let Some(node) = verified_node {
439 let (content, assertion) = decrypt_assertion_with_limits(
442 &node,
443 &load_key()?,
444 decrypt_options,
445 opts.xml_limits,
446 )?;
447 is_valid_xml_with_limits(&content, opts.xml_limits)?;
448 validate_protocol_profile(&content, parser_type, opts.xml_limits)?;
449 if assertion_signature == AssertionSignatureRequirement::Direct {
450 let decrypted_signature_present =
451 has_xml_signature_with_limits(&assertion, opts.xml_limits)?;
452 let (decrypted_verified, _, decrypted_assertion_covered) =
453 verify_embedded_signature(&assertion, opts, assertion_signature)?;
454 if !decrypted_verified {
455 return Err(required_xml_signature_failed(decrypted_signature_present));
456 }
457 require_direct_assertion_coverage(
458 assertion_signature,
459 decrypted_assertion_covered,
460 )?;
461 }
462 return Ok((content, Some(assertion)));
463 }
464 }
465 if decrypt_required && !verified {
466 let (content, assertion) =
468 decrypt_assertion_with_limits(xml, &load_key()?, decrypt_options, opts.xml_limits)?;
469 is_valid_xml_with_limits(&content, opts.xml_limits)?;
470 validate_protocol_profile(&content, parser_type, opts.xml_limits)?;
471 let verification_xml = if assertion_signature == AssertionSignatureRequirement::Direct {
472 assertion.as_str()
473 } else {
474 content.as_str()
475 };
476 let signature_present = has_xml_signature_with_limits(verification_xml, opts.xml_limits)?;
477 let (re_verified, re_node, re_assertion_directly_covered) =
478 verify_embedded_signature(verification_xml, opts, assertion_signature)?;
479 return if re_verified {
480 require_direct_assertion_coverage(assertion_signature, re_assertion_directly_covered)?;
481 let verified_assertion = if assertion_signature == AssertionSignatureRequirement::Direct
482 {
483 Some(assertion)
484 } else {
485 re_node
486 };
487 Ok((content, verified_assertion))
488 } else {
489 Err(required_xml_signature_failed(signature_present))
490 };
491 }
492 if verified {
493 require_direct_assertion_coverage(assertion_signature, assertion_directly_covered)?;
494 if matches!(
495 parser_type,
496 ParserType::SamlRequest | ParserType::LogoutRequest | ParserType::LogoutResponse
497 ) {
498 let content = verified_node.ok_or_else(verified_content_not_covered)?;
499 return Ok((content, None));
500 }
501 return Ok((xml.to_string(), verified_node));
502 }
503 Err(required_xml_signature_failed(signature_present))
504}
505
506#[cfg(not(feature = "crypto-bergshamra"))]
507fn verify_and_prepare(
508 _xml: &str,
509 _parser_type: ParserType,
510 _opts: &FlowOptions<'_>,
511 _assertion_signature: AssertionSignatureRequirement,
512) -> Result<(String, Option<String>), SamlError> {
513 Err(SamlError::Unsupported(
514 "signature verification requires feature crypto-bergshamra".into(),
515 ))
516}
517
518#[cfg(feature = "crypto-bergshamra")]
521fn verify_detached(
522 binding: Binding,
523 parser_type: ParserType,
524 request: &HttpRequest,
525 opts: &FlowOptions<'_>,
526 xml: &str,
527) -> Result<String, SamlError> {
528 let get = |k: &str| -> Result<Option<&str>, SamlError> {
529 match binding {
530 Binding::Redirect => request.query_get(k),
531 _ => request.body_get(k),
532 }
533 };
534 let signature = get("Signature")?.ok_or(SamlError::SignatureMissing)?;
535 let sig_alg = get("SigAlg")?.ok_or_else(|| missing_binding_parameter("SigAlg"))?;
536 let octet = request
537 .octet_string
538 .as_deref()
539 .ok_or_else(|| missing_binding_parameter("octet_string"))?;
540 ensure_detached_octet_matches_consumed_fields(
541 binding,
542 parser_type,
543 request,
544 xml,
545 sig_alg,
546 octet,
547 )?;
548 let verified = opts.signing_certs.iter().any(|cert| {
549 crate::crypto::verify_message_signature(octet, signature, cert, sig_alg).unwrap_or(false)
550 });
551 if verified {
552 Ok(sig_alg.to_string())
553 } else {
554 Err(detached_signature_verification())
555 }
556}
557
558#[cfg(not(feature = "crypto-bergshamra"))]
559fn verify_detached(
560 _binding: Binding,
561 _parser_type: ParserType,
562 _request: &HttpRequest,
563 _opts: &FlowOptions<'_>,
564 _xml: &str,
565) -> Result<String, SamlError> {
566 Err(SamlError::Unsupported(
567 "signature verification requires feature crypto-bergshamra".into(),
568 ))
569}
570
571fn audience_restriction_contains(
572 audience_restriction: &str,
573 expected: &str,
574 limits: XmlLimits,
575) -> Result<bool, SamlError> {
576 let field = ExtractorField::new("audience", &["AudienceRestriction", "Audience"]);
577 let extracted =
578 extract_with_limits(audience_restriction, std::slice::from_ref(&field), limits)?;
579 Ok(match extracted.get("audience") {
580 Some(Value::Str(audience)) => audience == expected,
581 Some(Value::Array(audiences)) => audiences
582 .iter()
583 .any(|audience| audience.as_str() == Some(expected)),
584 _ => false,
585 })
586}
587
588fn audience_restrictions_contain(
589 assertion: Option<&str>,
590 expected: &str,
591 limits: XmlLimits,
592) -> Result<bool, SamlError> {
593 let Some(assertion) = assertion else {
594 return Ok(false);
595 };
596 let field = ExtractorField::new(
597 "audienceRestriction",
598 &["Assertion", "Conditions", "AudienceRestriction"],
599 )
600 .with_context();
601 let extracted = extract_with_limits(assertion, std::slice::from_ref(&field), limits)?;
602
603 match extracted.get("audienceRestriction") {
604 Some(Value::Str(audience_restriction)) => {
605 audience_restriction_contains(audience_restriction, expected, limits)
606 }
607 Some(Value::Array(audience_restrictions)) if !audience_restrictions.is_empty() => {
608 for audience_restriction in audience_restrictions {
609 let Some(audience_restriction) = audience_restriction.as_str() else {
610 return Ok(false);
611 };
612 if !audience_restriction_contains(audience_restriction, expected, limits)? {
613 return Ok(false);
614 }
615 }
616 Ok(true)
617 }
618 _ => Ok(false),
619 }
620}
621
622fn subject_confirmation_xmls(extracted: &Value) -> Vec<&str> {
623 match extracted.get("subjectConfirmation") {
624 Some(Value::Str(xml)) => vec![xml.as_str()],
625 Some(Value::Array(items)) => items.iter().filter_map(Value::as_str).collect(),
626 _ => Vec::new(),
627 }
628}
629
630#[derive(Debug, Clone, Copy, PartialEq, Eq)]
631enum SubjectConfirmationCheck {
632 Valid,
633 Invalid(SubjectConfirmationReason),
634}
635
636fn check_bearer_subject_confirmation(
637 xml: &str,
638 opts: &FlowOptions<'_>,
639 expected_recipient: Option<&str>,
640) -> Result<SubjectConfirmationCheck, SamlError> {
641 let fields = [
642 ExtractorField::new("subjectConfirmation", &["SubjectConfirmation"]).attrs(&["Method"]),
643 ExtractorField::new(
644 "subjectConfirmationData",
645 &["SubjectConfirmation", "SubjectConfirmationData"],
646 )
647 .attrs(&["NotOnOrAfter", "Recipient", "InResponseTo"]),
648 ];
649 let extracted = extract_with_limits(xml, &fields, opts.xml_limits)?;
650
651 if extracted.get_str("subjectConfirmation") != Some(BEARER_SUBJECT_CONFIRMATION_METHOD) {
652 return Ok(SubjectConfirmationCheck::Invalid(
653 SubjectConfirmationReason::InvalidMethod,
654 ));
655 }
656
657 let Some(not_on_or_after) = extracted.get_str("subjectConfirmationData.notOnOrAfter") else {
658 return Ok(SubjectConfirmationCheck::Invalid(
659 SubjectConfirmationReason::MissingNotOnOrAfter,
660 ));
661 };
662 if !verify_time_at(
663 None,
664 Some(not_on_or_after),
665 opts.clock_drifts,
666 opts.validation_now()?,
667 ) {
668 return Ok(SubjectConfirmationCheck::Invalid(
669 SubjectConfirmationReason::TimeWindowInvalid,
670 ));
671 }
672
673 if let Some(expected) = expected_recipient {
674 if extracted.get_str("subjectConfirmationData.recipient") != Some(expected) {
675 return Ok(SubjectConfirmationCheck::Invalid(
676 SubjectConfirmationReason::RecipientMismatch,
677 ));
678 }
679 }
680
681 if let Some(expected) = opts.expected_in_response_to {
682 if extracted.get_str("subjectConfirmationData.inResponseTo") != Some(expected) {
683 return Ok(SubjectConfirmationCheck::Invalid(
684 SubjectConfirmationReason::InResponseToMismatch,
685 ));
686 }
687 }
688
689 Ok(SubjectConfirmationCheck::Valid)
690}
691
692fn validate_subject_confirmation(
693 extracted: &Value,
694 opts: &FlowOptions<'_>,
695 expected_recipient: Option<&str>,
696) -> Result<(), SamlError> {
697 let mut reason = None;
698 for xml in subject_confirmation_xmls(extracted) {
699 match check_bearer_subject_confirmation(xml, opts, expected_recipient)? {
700 SubjectConfirmationCheck::Valid => return Ok(()),
701 SubjectConfirmationCheck::Invalid(current) => reason = Some(current),
702 }
703 }
704 Err(SamlError::SubjectConfirmationInvalid {
705 reason: reason.unwrap_or(SubjectConfirmationReason::MissingBearerConfirmation),
706 })
707}
708
709fn validate_response_destination(
710 extracted: &Value,
711 expected_recipient: Option<&str>,
712) -> Result<(), SamlError> {
713 let Some(expected) = expected_recipient else {
714 return Ok(());
715 };
716 if let Some(destination) = extracted.get_str("response.destination") {
717 if destination != expected {
718 return Err(SamlError::destination_mismatch(expected, Some(destination)));
719 }
720 }
721 Ok(())
722}
723
724fn validate_context(
725 parser_type: ParserType,
726 assertion: Option<&str>,
727 extracted: &Value,
728 opts: &FlowOptions<'_>,
729 expected_recipient: Option<&str>,
730) -> Result<(), SamlError> {
731 let should_validate_issuer = matches!(
732 parser_type,
733 ParserType::SamlRequest
734 | ParserType::SamlResponse
735 | ParserType::LogoutRequest
736 | ParserType::LogoutResponse
737 );
738 if should_validate_issuer {
739 if let Some(expected) = opts.from_issuer {
740 let actual = extracted.get_str("issuer");
741 if actual != Some(expected) {
742 return Err(SamlError::issuer_mismatch(expected, actual));
743 }
744 }
745 }
746 let is_response = matches!(
747 parser_type,
748 ParserType::SamlResponse | ParserType::LogoutResponse
749 );
750 if is_response {
751 if let Some(expected) = opts.expected_in_response_to {
752 let actual = extracted.get_str("response.inResponseTo");
753 if actual != Some(expected) {
754 return Err(SamlError::in_response_to_mismatch(Some(expected), actual));
755 }
756 }
757 }
758 if parser_type == ParserType::SamlResponse {
759 validate_response_destination(extracted, expected_recipient)?;
760 validate_subject_confirmation(extracted, opts, expected_recipient)?;
761 if let Some(expected) = opts.expected_audience {
762 if !audience_restrictions_contain(assertion, expected, opts.xml_limits)? {
763 return Err(SamlError::AudienceMismatch {
764 expected: expected.to_string(),
765 });
766 }
767 }
768 let session_bounds = authn_statement_not_on_or_after_values(extracted)?;
769 if let Some(raw_expiration) =
770 earliest_authn_session_expiration(session_bounds, TimeWindowField::SessionNotOnOrAfter)?
771 {
772 let expiration = raw_expiration
773 .checked_add(Duration::milliseconds(opts.clock_drifts.1))
774 .ok_or(SamlError::TimeWindowInvalid {
775 field: TimeWindowField::SessionNotOnOrAfter,
776 })?;
777 if opts.validation_now()? >= expiration {
778 return Err(SamlError::TimeWindowInvalid {
779 field: TimeWindowField::SessionNotOnOrAfter,
780 });
781 }
782 }
783 let (not_before, not_on_or_after) = conditions_time_bounds(extracted)?;
784 if !verify_time_at(
785 not_before,
786 not_on_or_after,
787 opts.clock_drifts,
788 opts.validation_now()?,
789 ) {
790 return Err(SamlError::TimeWindowInvalid {
791 field: TimeWindowField::Conditions,
792 });
793 }
794 }
795 Ok(())
796}
797
798fn flow_inner(
799 opts: &FlowOptions<'_>,
800 request: &HttpRequest,
801 expected_recipient: Option<&str>,
802 assertion_signature: AssertionSignatureRequirement,
803) -> Result<FlowResult, SamlError> {
804 let binding = opts
805 .binding
806 .ok_or_else(|| missing_binding_parameter("binding"))?;
807 let parser_type = opts
808 .parser_type
809 .ok_or_else(|| SamlError::Invalid("ERR_UNDEFINED_PARSERTYPE".into()))?;
810
811 let xml = decode_message(
812 binding,
813 parser_type,
814 request,
815 opts.redirect_inflate_max_bytes,
816 opts.xml_limits,
817 )?;
818 is_valid_xml_with_limits(&xml, opts.xml_limits)?;
819 validate_protocol_profile(&xml, parser_type, opts.xml_limits)?;
820 check_status_with_limits(&xml, parser_type, opts.xml_limits)?;
821
822 let (saml_content, assertion, sig_alg) = if opts.check_signature {
823 match binding {
824 Binding::Redirect | Binding::SimpleSign => {
825 let sig_alg = verify_detached(binding, parser_type, request, opts, &xml)?;
826 let (saml_content, assertion) = if parser_type == ParserType::SamlResponse
827 && assertion_signature == AssertionSignatureRequirement::Direct
828 {
829 verify_and_prepare(&xml, parser_type, opts, assertion_signature)?
830 } else {
831 let assertion = if parser_type == ParserType::SamlResponse {
832 assertion_shortcut(&xml, opts.xml_limits)?
833 } else {
834 None
835 };
836 (xml, assertion)
837 };
838 (saml_content, assertion, Some(sig_alg))
839 }
840 _ => {
841 let (content, assertion) =
842 verify_and_prepare(&xml, parser_type, opts, assertion_signature)?;
843 (content, assertion, None)
844 }
845 }
846 } else {
847 let assertion = if parser_type == ParserType::SamlResponse {
848 assertion_shortcut(&xml, opts.xml_limits)?
849 } else {
850 None
851 };
852 (xml, assertion, None)
853 };
854
855 let fields = default_fields(parser_type, assertion.as_deref())?;
856 let extracted = extract_with_limits(&saml_content, &fields, opts.xml_limits)?;
857 validate_context(
858 parser_type,
859 assertion.as_deref(),
860 &extracted,
861 opts,
862 expected_recipient,
863 )?;
864
865 Ok(FlowResult {
866 saml_content,
867 extract: extracted,
868 sig_alg,
869 })
870}
871
872pub fn flow(opts: &FlowOptions<'_>, request: &HttpRequest) -> Result<FlowResult, SamlError> {
874 flow_inner(
875 opts,
876 request,
877 None,
878 AssertionSignatureRequirement::Compatible,
879 )
880}
881
882pub(crate) fn flow_with_expected_recipient(
883 opts: &FlowOptions<'_>,
884 request: &HttpRequest,
885 expected_recipient: &str,
886 assertion_signature: AssertionSignatureRequirement,
887) -> Result<FlowResult, SamlError> {
888 flow_inner(opts, request, Some(expected_recipient), assertion_signature)
889}