1use super::attributes::Attributes;
2use super::extract::{
3 attributes_from_extract, authn_sessions_from_extract, conditions_instants,
4 entity_ids_from_value, name_id_format_from_uri, optional_request_id, required_str,
5 subject_confirmations_from_extract,
6};
7use super::identifiers::{AssertionId, MessageId, SamlInstant};
8use super::session::{AuthnSession, EMPTY_AUTHN_SESSION};
9use super::subject::{NameId, Subject};
10use super::{
11 earliest_authn_session_expiration, LogoutSubject, ReplayKey, ReplayPolicy,
12 SamlValidationContext,
13};
14use crate::config::EntityId;
15use crate::error::{SamlError, TimeWindowField};
16use crate::raw::FlowResult;
17use crate::xml::{extract_with_limits, parse_saml_utc_date_time, ExtractorField, XmlLimits};
18use std::time::SystemTime;
19use time::{format_description::well_known::Rfc3339, Duration, OffsetDateTime};
20
21const BEARER_SUBJECT_CONFIRMATION_METHOD: &str = "urn:oasis:names:tc:SAML:2.0:cm:bearer";
22const REPLAY_EXPIRATION_FIELD: TimeWindowField = TimeWindowField::ReplayExpiration;
23
24#[derive(Debug, Clone)]
26pub struct SsoResponse {
27 response_id: MessageId,
28 issue_instant: SamlInstant,
29 issuer: EntityId,
30 in_response_to: Option<MessageId>,
31 raw_flow: FlowResult,
32}
33
34impl SsoResponse {
35 pub fn response_id(&self) -> &MessageId {
37 &self.response_id
38 }
39
40 pub fn issue_instant(&self) -> &SamlInstant {
42 &self.issue_instant
43 }
44
45 pub fn issuer(&self) -> &EntityId {
47 &self.issuer
48 }
49
50 pub fn in_response_to(&self) -> Option<&MessageId> {
52 self.in_response_to.as_ref()
53 }
54
55 pub fn raw_flow(&self) -> &FlowResult {
57 &self.raw_flow
58 }
59}
60
61impl TryFrom<FlowResult> for SsoResponse {
62 type Error = SamlError;
63
64 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
65 let response_id = MessageId::try_new(required_str(&raw_flow.extract, "response.id")?)?;
66 let issue_instant =
67 issue_instant_from_extract(&raw_flow.extract, "response.issueInstant", "Response")?;
68 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
69 let in_response_to = optional_request_id(&raw_flow.extract, "response.inResponseTo")?;
70 Ok(Self {
71 response_id,
72 issue_instant,
73 issuer,
74 in_response_to,
75 raw_flow,
76 })
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq)]
82pub struct Assertion {
83 id: Option<AssertionId>,
84 issuer: EntityId,
85 subject: Subject,
86}
87
88impl Assertion {
89 pub fn new(id: Option<AssertionId>, issuer: EntityId, subject: Subject) -> Self {
91 Self {
92 id,
93 issuer,
94 subject,
95 }
96 }
97
98 pub fn id(&self) -> Option<&AssertionId> {
100 self.id.as_ref()
101 }
102
103 pub fn issuer(&self) -> &EntityId {
105 &self.issuer
106 }
107
108 pub fn subject(&self) -> &Subject {
110 &self.subject
111 }
112}
113
114#[derive(Debug, Clone)]
116pub struct SsoSession {
117 response_id: MessageId,
118 response_issue_instant: SamlInstant,
119 assertion_id: AssertionId,
120 assertion_issue_instant: SamlInstant,
121 issuer: EntityId,
122 in_response_to: Option<MessageId>,
123 subject: Subject,
124 attributes: Attributes,
125 authn_sessions: Vec<AuthnSession>,
126 audience: Vec<EntityId>,
127 not_before: Option<SamlInstant>,
128 not_on_or_after: Option<SamlInstant>,
129 sig_alg: Option<String>,
130 raw_flow: FlowResult,
131}
132
133impl SsoSession {
134 pub fn response_id(&self) -> &MessageId {
136 &self.response_id
137 }
138
139 pub fn response_issue_instant(&self) -> &SamlInstant {
141 &self.response_issue_instant
142 }
143
144 pub fn assertion_id(&self) -> &AssertionId {
146 &self.assertion_id
147 }
148
149 pub fn assertion_issue_instant(&self) -> &SamlInstant {
151 &self.assertion_issue_instant
152 }
153
154 pub fn issuer(&self) -> &EntityId {
156 &self.issuer
157 }
158
159 pub fn in_response_to(&self) -> Option<&MessageId> {
161 self.in_response_to.as_ref()
162 }
163
164 pub fn subject(&self) -> &Subject {
166 &self.subject
167 }
168
169 pub fn name_id(&self) -> &NameId {
171 self.subject.name_id()
172 }
173
174 pub fn attributes(&self) -> &Attributes {
176 &self.attributes
177 }
178
179 pub fn authn_session(&self) -> &AuthnSession {
186 self.authn_sessions.first().unwrap_or(&EMPTY_AUTHN_SESSION)
187 }
188
189 pub fn authn_sessions(&self) -> &[AuthnSession] {
191 &self.authn_sessions
192 }
193
194 pub fn audience(&self) -> &[EntityId] {
196 &self.audience
197 }
198
199 pub fn not_before(&self) -> Option<&SamlInstant> {
201 self.not_before.as_ref()
202 }
203
204 pub fn not_on_or_after(&self) -> Option<&SamlInstant> {
206 self.not_on_or_after.as_ref()
207 }
208
209 pub fn sig_alg(&self) -> Option<&str> {
211 self.sig_alg.as_deref()
212 }
213
214 pub fn assertion(&self) -> Assertion {
216 Assertion::new(
217 Some(self.assertion_id.clone()),
218 self.issuer.clone(),
219 self.subject.clone(),
220 )
221 }
222
223 pub fn logout_subject(&self) -> Option<LogoutSubject> {
248 if self.name_id().value().trim().is_empty() {
249 return None;
250 }
251 let session_indexes = self
252 .authn_sessions
253 .iter()
254 .filter_map(AuthnSession::session_index)
255 .cloned()
256 .collect();
257 Some(LogoutSubject::new(self.name_id().clone(), session_indexes))
258 }
259
260 pub fn replay_keys(&self) -> Vec<ReplayKey> {
262 vec![
263 ReplayKey::ResponseId(self.response_id.clone()),
264 ReplayKey::AssertionId(self.assertion_id.clone()),
265 ]
266 }
267
268 pub fn check_and_store_replay(
284 &self,
285 validation: &mut SamlValidationContext<'_>,
286 ) -> Result<(), SamlError> {
287 let validation_now = validation.now_offset()?;
288 let not_on_or_after_skew_ms = validation.clock_skew().not_on_or_after_millis();
289 match validation.replay_policy() {
290 ReplayPolicy::DisabledForCompatibility => Ok(()),
291 ReplayPolicy::RequireCache(cache) => {
292 let expires_at = self.replay_expires_at(validation_now, not_on_or_after_skew_ms)?;
293 let since_epoch = expires_at - OffsetDateTime::UNIX_EPOCH;
294 let expires_at = if since_epoch.is_negative() {
295 SystemTime::UNIX_EPOCH.checked_sub(since_epoch.unsigned_abs())
296 } else {
297 SystemTime::UNIX_EPOCH.checked_add(since_epoch.unsigned_abs())
298 }
299 .ok_or(SamlError::TimeWindowInvalid {
300 field: REPLAY_EXPIRATION_FIELD,
301 })?;
302 let keys = self.replay_keys();
303 for key in keys {
304 cache.check_and_store(key, expires_at)?;
305 }
306 Ok(())
307 }
308 }
309 }
310
311 pub fn raw_flow(&self) -> &FlowResult {
313 &self.raw_flow
314 }
315
316 fn replay_expires_at(
317 &self,
318 validation_now: OffsetDateTime,
319 not_on_or_after_skew_ms: i64,
320 ) -> Result<OffsetDateTime, SamlError> {
321 let mut candidates = Vec::with_capacity(3);
322 if let Some(instant) = self.not_on_or_after() {
323 candidates.push(parse_replay_expiration(instant.as_str())?);
324 }
325 if let Some(instant) = earliest_authn_session_expiration(
326 self.authn_sessions
327 .iter()
328 .filter_map(AuthnSession::not_on_or_after)
329 .map(SamlInstant::as_str),
330 REPLAY_EXPIRATION_FIELD,
331 )? {
332 candidates.push(instant);
333 }
334 if let Some(instant) = self.bearer_subject_confirmation_expires_at()? {
335 candidates.push(instant);
336 }
337
338 let raw_expires_at = candidates
339 .into_iter()
340 .min()
341 .ok_or(SamlError::TimeWindowInvalid {
342 field: REPLAY_EXPIRATION_FIELD,
343 })?;
344 let expires_at = raw_expires_at
345 .checked_add(Duration::milliseconds(not_on_or_after_skew_ms))
346 .ok_or(SamlError::TimeWindowInvalid {
347 field: REPLAY_EXPIRATION_FIELD,
348 })?;
349 if validation_now >= expires_at {
350 return Err(SamlError::TimeWindowInvalid {
351 field: REPLAY_EXPIRATION_FIELD,
352 });
353 }
354 Ok(expires_at)
355 }
356
357 fn bearer_subject_confirmation_expires_at(&self) -> Result<Option<OffsetDateTime>, SamlError> {
358 let fields = [
359 ExtractorField::new("subjectConfirmation", &["SubjectConfirmation"]).attrs(&["Method"]),
360 ExtractorField::new(
361 "subjectConfirmationData",
362 &["SubjectConfirmation", "SubjectConfirmationData"],
363 )
364 .attrs(&["NotOnOrAfter"]),
365 ];
366 let mut expires_at = None;
367 for confirmation in self.subject.confirmations() {
368 let extracted =
369 extract_with_limits(confirmation.raw_xml(), &fields, XmlLimits::default())?;
370 if extracted.get_str("subjectConfirmation") != Some(BEARER_SUBJECT_CONFIRMATION_METHOD)
371 {
372 continue;
373 }
374 let Some(not_on_or_after) = extracted.get_str("subjectConfirmationData") else {
375 continue;
376 };
377 let candidate = parse_replay_expiration(not_on_or_after)?;
378 match expires_at {
379 Some(current) if current >= candidate => {}
380 Some(_) | None => expires_at = Some(candidate),
381 }
382 }
383 Ok(expires_at)
384 }
385}
386
387fn parse_replay_expiration(value: &str) -> Result<OffsetDateTime, SamlError> {
388 OffsetDateTime::parse(value, &Rfc3339).map_err(|_| SamlError::TimeWindowInvalid {
389 field: REPLAY_EXPIRATION_FIELD,
390 })
391}
392
393impl TryFrom<FlowResult> for SsoSession {
394 type Error = SamlError;
395
396 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
397 let response_id = MessageId::try_new(required_str(&raw_flow.extract, "response.id")?)?;
398 let response_issue_instant =
399 issue_instant_from_extract(&raw_flow.extract, "response.issueInstant", "Response")?;
400 let assertion_id = AssertionId::try_new(required_str(&raw_flow.extract, "assertion.id")?)?;
401 let assertion_issue_instant =
402 issue_instant_from_extract(&raw_flow.extract, "assertion.issueInstant", "Assertion")?;
403 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
404 let in_response_to = optional_request_id(&raw_flow.extract, "response.inResponseTo")?;
405 let name_id_format = raw_flow
406 .extract
407 .get_str("nameIDFormat")
408 .map(name_id_format_from_uri);
409 let name_id = NameId::new(required_str(&raw_flow.extract, "nameID")?, name_id_format);
410 let subject = Subject::new(
411 name_id,
412 subject_confirmations_from_extract(&raw_flow.extract),
413 );
414 let attributes = attributes_from_extract(&raw_flow.extract);
415 let authn_sessions = authn_sessions_from_extract(&raw_flow.extract)?;
416 let audience = entity_ids_from_value(raw_flow.extract.get("audience"))?;
417 let (not_before, not_on_or_after) = conditions_instants(&raw_flow.extract)?;
418 let sig_alg = raw_flow.sig_alg.clone();
419 Ok(Self {
420 response_id,
421 response_issue_instant,
422 assertion_id,
423 assertion_issue_instant,
424 issuer,
425 in_response_to,
426 subject,
427 attributes,
428 authn_sessions,
429 audience,
430 not_before,
431 not_on_or_after,
432 sig_alg,
433 raw_flow,
434 })
435 }
436}
437
438fn issue_instant_from_extract(
439 extract: &crate::util::Value,
440 path: &str,
441 element: &str,
442) -> Result<SamlInstant, SamlError> {
443 let issue_instant = extract.get_str(path).ok_or_else(|| {
444 SamlError::ProtocolProfile(format!(
445 "{element} is missing required unqualified attribute IssueInstant"
446 ))
447 })?;
448 let issue_instant = parse_saml_utc_date_time(issue_instant).ok_or_else(|| {
449 SamlError::ProtocolProfile(format!(
450 "{element} IssueInstant must use the SAML-conformant UTC xs:dateTime form ending in Z"
451 ))
452 })?;
453 SamlInstant::try_new(issue_instant)
454}