Skip to main content

saml_rs/model/
authn.rs

1use super::extract::{name_id_policy_from_extract, optional_endpoint, optional_u16, required_str};
2use super::identifiers::{MessageId, SamlInstant};
3use super::subject::NameIdPolicy;
4use super::{EndpointUrl, ReplayKey, SamlValidationContext};
5use crate::browser::SsoResponseBinding;
6use crate::config::EntityId;
7use crate::constants::Binding;
8use crate::error::SamlError;
9use crate::raw::FlowResult;
10use crate::xml::parse_saml_utc_date_time;
11
12/// Parsed AuthnRequest result.
13#[derive(Debug, Clone)]
14pub struct AuthnRequest {
15    id: MessageId,
16    issue_instant: SamlInstant,
17    issuer: EntityId,
18    destination: Option<EndpointUrl>,
19    acs_url: Option<EndpointUrl>,
20    protocol_binding: Option<SsoResponseBinding>,
21    acs_index: Option<u16>,
22    name_id_policy: Option<NameIdPolicy>,
23    raw_flow: FlowResult,
24}
25
26impl AuthnRequest {
27    /// Request ID.
28    pub fn id(&self) -> &MessageId {
29        &self.id
30    }
31
32    /// Request `IssueInstant`, normalized according to XML Schema whitespace rules.
33    pub fn issue_instant(&self) -> &SamlInstant {
34        &self.issue_instant
35    }
36
37    /// Request issuer.
38    pub fn issuer(&self) -> &EntityId {
39        &self.issuer
40    }
41
42    /// Destination endpoint, when present.
43    pub fn destination(&self) -> Option<&EndpointUrl> {
44        self.destination.as_ref()
45    }
46
47    /// AssertionConsumerServiceURL, when present.
48    pub fn acs_url(&self) -> Option<&EndpointUrl> {
49        self.acs_url.as_ref()
50    }
51
52    /// Requested response `ProtocolBinding`, when present.
53    pub fn protocol_binding(&self) -> Option<SsoResponseBinding> {
54        self.protocol_binding
55    }
56
57    /// Requested `AssertionConsumerServiceIndex`, when present.
58    pub fn acs_index(&self) -> Option<u16> {
59        self.acs_index
60    }
61
62    /// NameIDPolicy, when present.
63    pub fn name_id_policy(&self) -> Option<&NameIdPolicy> {
64        self.name_id_policy.as_ref()
65    }
66
67    /// Check and store this AuthnRequest's replay key using caller cache state.
68    ///
69    /// # Errors
70    ///
71    /// Returns [`SamlError::ReplayDetected`] when the request ID has already
72    /// been seen. Cache implementations may also return storage-specific
73    /// failures mapped to [`SamlError`].
74    pub fn check_and_store_replay(
75        &self,
76        validation: &mut SamlValidationContext<'_>,
77    ) -> Result<(), SamlError> {
78        validation.check_and_store_message_replay(ReplayKey::AuthnRequestId(self.id.clone()))
79    }
80
81    /// Raw validated flow result.
82    pub fn raw_flow(&self) -> &FlowResult {
83        &self.raw_flow
84    }
85}
86
87impl TryFrom<FlowResult> for AuthnRequest {
88    type Error = SamlError;
89
90    fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
91        let id = MessageId::try_new(required_str(&raw_flow.extract, "request.id")?)?;
92        let issue_instant = issue_instant_from_extract(&raw_flow.extract)?;
93        let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
94        let destination = optional_endpoint(&raw_flow.extract, "request.destination")?;
95        let acs_url = optional_endpoint(&raw_flow.extract, "request.assertionConsumerServiceUrl")?;
96        let protocol_binding = optional_response_binding(&raw_flow.extract)?;
97        let acs_index = optional_u16(&raw_flow.extract, "request.assertionConsumerServiceIndex")?;
98        let name_id_policy = name_id_policy_from_extract(&raw_flow.extract)?;
99        Ok(Self {
100            id,
101            issue_instant,
102            issuer,
103            destination,
104            acs_url,
105            protocol_binding,
106            acs_index,
107            name_id_policy,
108            raw_flow,
109        })
110    }
111}
112
113fn issue_instant_from_extract(extract: &crate::util::Value) -> Result<SamlInstant, SamlError> {
114    let issue_instant = extract.get_str("request.issueInstant").ok_or_else(|| {
115        SamlError::ProtocolProfile(
116            "AuthnRequest is missing required unqualified attribute IssueInstant".into(),
117        )
118    })?;
119    let issue_instant = parse_saml_utc_date_time(issue_instant).ok_or_else(|| {
120        SamlError::ProtocolProfile(
121            "AuthnRequest IssueInstant must use the SAML-conformant UTC xs:dateTime form ending in Z"
122                .into(),
123        )
124    })?;
125    SamlInstant::try_new(issue_instant)
126}
127
128fn optional_response_binding(
129    extract: &crate::util::Value,
130) -> Result<Option<SsoResponseBinding>, SamlError> {
131    let Some(protocol_binding) = extract.get_str("request.protocolBinding") else {
132        return Ok(None);
133    };
134    let binding = Binding::from_urn(protocol_binding).ok_or_else(|| {
135        SamlError::Invalid(format!(
136            "unsupported AuthnRequest ProtocolBinding {protocol_binding}"
137        ))
138    })?;
139    SsoResponseBinding::try_from(binding).map(Some)
140}