1use super::extract::{
2 name_id_format_from_uri, name_id_policy_from_extract, optional_endpoint, optional_u16,
3 required_str, subject_confirmations_at,
4};
5use super::identifiers::{MessageId, SamlInstant};
6use super::subject::{NameId, NameIdPolicy, RequestedSubject, RequestedSubjectIdentifier};
7use super::{EndpointUrl, ReplayKey, SamlValidationContext};
8use crate::browser::SsoResponseBinding;
9use crate::config::EntityId;
10use crate::constants::Binding;
11use crate::error::SamlError;
12use crate::raw::FlowResult;
13use crate::xml::parse_saml_utc_date_time;
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ForceAuthn {
23 Required,
25 NotRequired,
27}
28
29impl ForceAuthn {
30 pub(crate) fn from_bool(value: bool) -> Self {
31 if value {
32 Self::Required
33 } else {
34 Self::NotRequired
35 }
36 }
37
38 pub(crate) fn as_bool(self) -> bool {
39 match self {
40 Self::Required => true,
41 Self::NotRequired => false,
42 }
43 }
44}
45
46#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum IsPassive {
53 Required,
55 NotRequired,
57}
58
59impl IsPassive {
60 pub(crate) fn from_bool(value: bool) -> Self {
61 if value {
62 Self::Required
63 } else {
64 Self::NotRequired
65 }
66 }
67}
68
69#[derive(Debug, Clone)]
71pub struct AuthnRequest {
72 id: MessageId,
73 issue_instant: SamlInstant,
74 issuer: EntityId,
75 destination: Option<EndpointUrl>,
76 acs_url: Option<EndpointUrl>,
77 protocol_binding: Option<SsoResponseBinding>,
78 acs_index: Option<u16>,
79 name_id_policy: Option<NameIdPolicy>,
80 force_authn: Option<ForceAuthn>,
81 is_passive: Option<IsPassive>,
82 requested_subject: Option<RequestedSubject>,
83 raw_flow: FlowResult,
84}
85
86impl AuthnRequest {
87 pub fn id(&self) -> &MessageId {
89 &self.id
90 }
91
92 pub fn issue_instant(&self) -> &SamlInstant {
94 &self.issue_instant
95 }
96
97 pub fn issuer(&self) -> &EntityId {
99 &self.issuer
100 }
101
102 pub fn destination(&self) -> Option<&EndpointUrl> {
104 self.destination.as_ref()
105 }
106
107 pub fn acs_url(&self) -> Option<&EndpointUrl> {
109 self.acs_url.as_ref()
110 }
111
112 pub fn protocol_binding(&self) -> Option<SsoResponseBinding> {
114 self.protocol_binding
115 }
116
117 pub fn acs_index(&self) -> Option<u16> {
119 self.acs_index
120 }
121
122 pub fn name_id_policy(&self) -> Option<&NameIdPolicy> {
124 self.name_id_policy.as_ref()
125 }
126
127 pub fn force_authn(&self) -> Option<ForceAuthn> {
129 self.force_authn
130 }
131
132 pub fn is_passive(&self) -> Option<IsPassive> {
134 self.is_passive
135 }
136
137 pub fn requested_subject(&self) -> Option<&RequestedSubject> {
139 self.requested_subject.as_ref()
140 }
141
142 pub fn check_and_store_replay(
150 &self,
151 validation: &mut SamlValidationContext<'_>,
152 ) -> Result<(), SamlError> {
153 validation.check_and_store_message_replay(ReplayKey::AuthnRequestId(self.id.clone()))
154 }
155
156 pub fn raw_flow(&self) -> &FlowResult {
158 &self.raw_flow
159 }
160}
161
162impl TryFrom<FlowResult> for AuthnRequest {
163 type Error = SamlError;
164
165 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
166 let id = MessageId::try_new(required_str(&raw_flow.extract, "request.id")?)?;
167 let issue_instant = issue_instant_from_extract(&raw_flow.extract)?;
168 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
169 let destination = optional_endpoint(&raw_flow.extract, "request.destination")?;
170 let acs_url = optional_endpoint(&raw_flow.extract, "request.assertionConsumerServiceUrl")?;
171 let protocol_binding = optional_response_binding(&raw_flow.extract)?;
172 let acs_index = optional_u16(&raw_flow.extract, "request.assertionConsumerServiceIndex")?;
173 let name_id_policy = name_id_policy_from_extract(&raw_flow.extract)?;
174 let force_authn = optional_flag(&raw_flow.extract, "request.forceAuthn", "ForceAuthn")?
175 .map(ForceAuthn::from_bool);
176 let is_passive = optional_flag(&raw_flow.extract, "request.isPassive", "IsPassive")?
177 .map(IsPassive::from_bool);
178 let requested_subject = requested_subject_from_extract(&raw_flow.extract)?;
179 Ok(Self {
180 id,
181 issue_instant,
182 issuer,
183 destination,
184 acs_url,
185 protocol_binding,
186 acs_index,
187 name_id_policy,
188 force_authn,
189 is_passive,
190 requested_subject,
191 raw_flow,
192 })
193 }
194}
195
196fn optional_flag(
197 extract: &crate::util::Value,
198 path: &str,
199 label: &str,
200) -> Result<Option<bool>, SamlError> {
201 let Some(value) = extract.get_str(path) else {
202 return Ok(None);
203 };
204 match value {
205 "true" | "1" => Ok(Some(true)),
206 "false" | "0" => Ok(Some(false)),
207 _ => Err(SamlError::Invalid(format!(
208 "AuthnRequest {label} must be an XML Schema boolean (true, false, 1, or 0)"
209 ))),
210 }
211}
212
213fn requested_subject_from_extract(
214 extract: &crate::util::Value,
215) -> Result<Option<RequestedSubject>, SamlError> {
216 if single_text(extract, "requestedSubject", "AuthnRequest Subject")?.is_none() {
217 return Ok(None);
218 }
219 let identifier = requested_identifier(extract)?;
220 let confirmations = subject_confirmations_at(extract, "requestedSubjectConfirmation");
221 Ok(Some(RequestedSubject::new(identifier, confirmations)))
222}
223
224fn requested_identifier(
225 extract: &crate::util::Value,
226) -> Result<RequestedSubjectIdentifier, SamlError> {
227 let name_id = single_text(extract, "requestedNameId", "AuthnRequest NameID")?;
228 let base_id = single_text(extract, "requestedBaseId", "AuthnRequest BaseID")?;
229 let encrypted_id = single_text(extract, "requestedEncryptedId", "AuthnRequest EncryptedID")?;
230 let identifiers = [name_id, base_id, encrypted_id]
231 .into_iter()
232 .filter(Option::is_some)
233 .count();
234 if identifiers > 1 {
235 return Err(SamlError::Invalid(
236 "AuthnRequest Subject must contain one identifier element".into(),
237 ));
238 }
239 if let Some(value) = name_id {
240 return Ok(RequestedSubjectIdentifier::NameId(name_id_from_extract(
241 extract, value,
242 )?));
243 }
244 if base_id.is_some() {
245 return Ok(RequestedSubjectIdentifier::BaseId);
246 }
247 if encrypted_id.is_some() {
248 return Ok(RequestedSubjectIdentifier::EncryptedId);
249 }
250 Ok(RequestedSubjectIdentifier::NoIdentifier)
251}
252
253fn name_id_from_extract(extract: &crate::util::Value, value: &str) -> Result<NameId, SamlError> {
254 let format = single_text(
255 extract,
256 "requestedNameIdFormat",
257 "AuthnRequest NameID Format",
258 )?
259 .map(name_id_format_from_uri);
260 Ok(NameId::with_qualifiers(
261 value.to_string(),
262 format,
263 optional_attribute(extract, "requestedNameQualifier", "NameQualifier")?,
264 optional_attribute(extract, "requestedSpNameQualifier", "SPNameQualifier")?,
265 optional_attribute(extract, "requestedSpProvidedId", "SPProvidedID")?,
266 ))
267}
268
269fn optional_attribute(
270 extract: &crate::util::Value,
271 path: &str,
272 label: &str,
273) -> Result<Option<String>, SamlError> {
274 Ok(single_text(extract, path, label)?.map(str::to_string))
275}
276
277fn single_text<'a>(
278 extract: &'a crate::util::Value,
279 path: &str,
280 label: &str,
281) -> Result<Option<&'a str>, SamlError> {
282 match extract.get(path) {
283 None | Some(crate::util::Value::Null) => Ok(None),
284 Some(crate::util::Value::Str(value)) => Ok(Some(value.as_str())),
285 Some(crate::util::Value::Array(_) | crate::util::Value::Object(_)) => {
286 Err(SamlError::Invalid(format!("{label} must occur once")))
287 }
288 }
289}
290
291fn issue_instant_from_extract(extract: &crate::util::Value) -> Result<SamlInstant, SamlError> {
292 let issue_instant = extract.get_str("request.issueInstant").ok_or_else(|| {
293 SamlError::ProtocolProfile(
294 "AuthnRequest is missing required unqualified attribute IssueInstant".into(),
295 )
296 })?;
297 let issue_instant = parse_saml_utc_date_time(issue_instant).ok_or_else(|| {
298 SamlError::ProtocolProfile(
299 "AuthnRequest IssueInstant must use the SAML-conformant UTC xs:dateTime form ending in Z"
300 .into(),
301 )
302 })?;
303 SamlInstant::try_new(issue_instant)
304}
305
306fn optional_response_binding(
307 extract: &crate::util::Value,
308) -> Result<Option<SsoResponseBinding>, SamlError> {
309 let Some(protocol_binding) = extract.get_str("request.protocolBinding") else {
310 return Ok(None);
311 };
312 let binding = Binding::from_urn(protocol_binding).ok_or_else(|| {
313 SamlError::Invalid(format!(
314 "unsupported AuthnRequest ProtocolBinding {protocol_binding}"
315 ))
316 })?;
317 SsoResponseBinding::try_from(binding).map(Some)
318}