Skip to main content

saml_rs/
flow.rs

1//! Inbound message flow: decode, validate XML/status, verify signatures,
2//! optionally decrypt, extract fields, and validate issuer/time constraints.
3
4use 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/// Decoded HTTP request inputs for a binding.
26#[derive(Debug, Default, Clone)]
27pub struct HttpRequest {
28    /// URL-decoded query parameters (HTTP-Redirect).
29    pub query: Vec<(String, String)>,
30    /// Form body parameters (HTTP-POST / SimpleSign).
31    pub body: Vec<(String, String)>,
32    /// Signed octet string for detached-signature verification.
33    pub octet_string: Option<String>,
34}
35
36impl HttpRequest {
37    /// HTTP-Redirect request from query pairs.
38    pub fn redirect(query: Vec<(String, String)>) -> Self {
39        Self {
40            query,
41            ..Default::default()
42        }
43    }
44
45    /// HTTP-POST/SimpleSign request from body pairs.
46    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/// Inputs controlling a flow run.
86#[non_exhaustive]
87#[derive(Debug, Clone)]
88pub struct FlowOptions<'a> {
89    /// Protocol binding.
90    pub binding: Option<Binding>,
91    /// Message parser type.
92    pub parser_type: Option<ParserType>,
93    /// Maximum decoded compressed and inflated raw-DEFLATE bytes accepted for
94    /// HTTP-Redirect input.
95    pub redirect_inflate_max_bytes: usize,
96    /// XML parser resource limits for decoded messages and DOM reparses.
97    pub xml_limits: XmlLimits,
98    /// Whether to require and verify a signature.
99    pub check_signature: bool,
100    /// Expected issuer (peer `entityID`).
101    pub from_issuer: Option<&'a str>,
102    /// Peer signing certificate(s) for verification.
103    pub signing_certs: &'a [String],
104    /// Our decryption private key PEM (when assertions are encrypted).
105    pub decrypt_key: Option<&'a str>,
106    /// Passphrase for `decrypt_key`.
107    pub decrypt_key_pass: Option<&'a str>,
108    /// Allow XML-Enc RSA key-transport decryption with the bundled software RSA backend.
109    ///
110    /// Disabled by default because that path reaches `RUSTSEC-2023-0071`-affected
111    /// code when an attacker can observe timing.
112    pub allow_insecure_software_rsa_key_transport_decryption: bool,
113    /// Clock drift tolerance `(not_before_ms, not_on_or_after_ms)`.
114    pub clock_drifts: (i64, i64),
115    /// Validation instant. `None` keeps raw compatibility behavior by reading
116    /// the process clock during validation.
117    pub now: Option<SystemTime>,
118    /// Expected `<Audience>` (this SP's entity ID); `None` skips the check.
119    pub expected_audience: Option<&'a str>,
120    /// Expected `InResponseTo` (originating request ID); `None` skips the check.
121    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/// Result of a successful flow.
161#[derive(Debug, Clone)]
162pub struct FlowResult {
163    /// The decoded (and, when verified, authenticated) SAML XML.
164    pub saml_content: String,
165    /// Extracted fields.
166    pub extract: Value,
167    /// Verified signature algorithm, if a signature was checked.
168    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        // Older saml-rs outbound SimpleSign signed an empty RelayState field
320        // even when the form body omitted RelayState; keep accepting it for
321        // compatibility.
322        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/// Verify and optionally decrypt the message, returning the authenticated
409/// `(saml_content, assertion)`. Requires `crypto-bergshamra`.
410#[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            // signed-then-encrypted: the verified content is a Response carrying
440            // an EncryptedAssertion.
441            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        // encrypted-then-signed: decrypt first, then verify the result.
467        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/// Verify a detached (redirect/SimpleSign) message signature, returning the
519/// verified `SigAlg`. Requires `crypto-bergshamra`.
520#[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
872/// Run the inbound flow described by `opts` against `request`.
873pub 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}