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, Copy, PartialEq, Eq)]
116pub enum VerifiedXmlSignatureCoverage {
117 ResponseRoot,
119 ConsumedAssertion,
121 ResponseRootAndConsumedAssertion,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq)]
132pub struct VerifiedXmlSignature {
133 algorithm_uri: String,
134 coverage: VerifiedXmlSignatureCoverage,
135}
136
137impl VerifiedXmlSignature {
138 pub(crate) fn from_verified_coverage(
139 algorithm_uri: String,
140 response_covered: bool,
141 assertion_directly_covered: bool,
142 ) -> Option<Self> {
143 let coverage = match (response_covered, assertion_directly_covered) {
144 (true, true) => VerifiedXmlSignatureCoverage::ResponseRootAndConsumedAssertion,
145 (true, false) => VerifiedXmlSignatureCoverage::ResponseRoot,
146 (false, true) => VerifiedXmlSignatureCoverage::ConsumedAssertion,
147 (false, false) => return None,
148 };
149 Some(Self {
150 algorithm_uri,
151 coverage,
152 })
153 }
154
155 pub fn algorithm_uri(&self) -> &str {
157 &self.algorithm_uri
158 }
159
160 pub fn coverage(&self) -> VerifiedXmlSignatureCoverage {
163 self.coverage
164 }
165}
166
167#[derive(Debug, Clone)]
169pub struct SsoSession {
170 response_id: MessageId,
171 response_issue_instant: SamlInstant,
172 assertion_id: AssertionId,
173 assertion_issue_instant: SamlInstant,
174 issuer: EntityId,
175 in_response_to: Option<MessageId>,
176 subject: Subject,
177 attributes: Attributes,
178 authn_sessions: Vec<AuthnSession>,
179 audience: Vec<EntityId>,
180 not_before: Option<SamlInstant>,
181 not_on_or_after: Option<SamlInstant>,
182 sig_alg: Option<String>,
183 verified_xml_signatures: Vec<VerifiedXmlSignature>,
184 raw_flow: FlowResult,
185}
186
187impl SsoSession {
188 pub fn response_id(&self) -> &MessageId {
190 &self.response_id
191 }
192
193 pub fn response_issue_instant(&self) -> &SamlInstant {
195 &self.response_issue_instant
196 }
197
198 pub fn assertion_id(&self) -> &AssertionId {
200 &self.assertion_id
201 }
202
203 pub fn assertion_issue_instant(&self) -> &SamlInstant {
205 &self.assertion_issue_instant
206 }
207
208 pub fn issuer(&self) -> &EntityId {
210 &self.issuer
211 }
212
213 pub fn in_response_to(&self) -> Option<&MessageId> {
215 self.in_response_to.as_ref()
216 }
217
218 pub fn subject(&self) -> &Subject {
220 &self.subject
221 }
222
223 pub fn name_id(&self) -> &NameId {
225 self.subject.name_id()
226 }
227
228 pub fn attributes(&self) -> &Attributes {
230 &self.attributes
231 }
232
233 pub fn authn_session(&self) -> &AuthnSession {
240 self.authn_sessions.first().unwrap_or(&EMPTY_AUTHN_SESSION)
241 }
242
243 pub fn authn_sessions(&self) -> &[AuthnSession] {
245 &self.authn_sessions
246 }
247
248 pub fn audience(&self) -> &[EntityId] {
250 &self.audience
251 }
252
253 pub fn not_before(&self) -> Option<&SamlInstant> {
255 self.not_before.as_ref()
256 }
257
258 pub fn not_on_or_after(&self) -> Option<&SamlInstant> {
260 self.not_on_or_after.as_ref()
261 }
262
263 pub fn sig_alg(&self) -> Option<&str> {
269 self.sig_alg.as_deref()
270 }
271
272 pub fn verified_xml_signatures(&self) -> &[VerifiedXmlSignature] {
281 &self.verified_xml_signatures
282 }
283
284 pub fn assertion(&self) -> Assertion {
286 Assertion::new(
287 Some(self.assertion_id.clone()),
288 self.issuer.clone(),
289 self.subject.clone(),
290 )
291 }
292
293 pub fn logout_subject(&self) -> Option<LogoutSubject> {
318 if self.name_id().value().trim().is_empty() {
319 return None;
320 }
321 let session_indexes = self
322 .authn_sessions
323 .iter()
324 .filter_map(AuthnSession::session_index)
325 .cloned()
326 .collect();
327 Some(LogoutSubject::new(self.name_id().clone(), session_indexes))
328 }
329
330 pub fn replay_keys(&self) -> Vec<ReplayKey> {
332 let mut keys = vec![ReplayKey::ResponseId(self.response_id.clone())];
333 let mut ids = crate::assertion_acceptance::assertion_ids(&self.raw_flow.saml_content)
334 .unwrap_or_default();
335 if !ids.iter().any(|id| id == self.assertion_id.as_str()) {
336 ids.insert(0, self.assertion_id.as_str().to_string());
337 }
338 for id in ids {
339 if let Ok(id) = AssertionId::try_new(id) {
340 keys.push(ReplayKey::AssertionId(id));
341 }
342 }
343 keys
344 }
345
346 pub fn check_and_store_replay(
368 &self,
369 validation: &mut SamlValidationContext<'_>,
370 ) -> Result<(), SamlError> {
371 let validation_now = validation.now_offset()?;
372 let not_on_or_after_skew_ms = validation.clock_skew().not_on_or_after_millis();
373 match validation.replay_policy() {
374 ReplayPolicy::DisabledForCompatibility => Ok(()),
375 ReplayPolicy::RequireCache(cache) => {
376 let replays =
377 crate::assertion_acceptance::assertion_replays(&self.raw_flow.saml_content)?;
378 let response_expires = system_time_from_offset(replay_deadline(
379 self.earliest_replay_instant()?,
380 validation_now,
381 not_on_or_after_skew_ms,
382 )?)?;
383 let assertion_expires = system_time_from_offset(replay_deadline(
384 self.assertion_replay_instant()?,
385 validation_now,
386 not_on_or_after_skew_ms,
387 )?)?;
388 cache.check_and_store(
389 ReplayKey::ResponseId(self.response_id.clone()),
390 response_expires,
391 )?;
392 cache.check_and_store(
393 ReplayKey::AssertionId(self.assertion_id.clone()),
394 assertion_expires,
395 )?;
396 for replay in replays {
397 if replay.id == self.assertion_id.as_str() {
398 continue;
399 }
400 let expires_at = match replay.bearer_not_on_or_after {
401 Some(instant) => system_time_from_offset(replay_deadline(
402 parse_replay_expiration(&instant)?,
403 validation_now,
404 not_on_or_after_skew_ms,
405 )?)?,
406 None => assertion_expires,
407 };
408 cache.check_and_store(
409 ReplayKey::AssertionId(AssertionId::try_new(replay.id)?),
410 expires_at,
411 )?;
412 }
413 Ok(())
414 }
415 }
416 }
417
418 pub fn raw_flow(&self) -> &FlowResult {
420 &self.raw_flow
421 }
422
423 fn earliest_replay_instant(&self) -> Result<OffsetDateTime, SamlError> {
424 let mut candidates = Vec::with_capacity(3);
425 if let Some(instant) = self.not_on_or_after() {
426 candidates.push(parse_replay_expiration(instant.as_str())?);
427 }
428 if let Some(instant) = earliest_authn_session_expiration(
429 self.authn_sessions
430 .iter()
431 .filter_map(AuthnSession::not_on_or_after)
432 .map(SamlInstant::as_str),
433 REPLAY_EXPIRATION_FIELD,
434 )? {
435 candidates.push(instant);
436 }
437 if let Some(instant) = self.bearer_subject_confirmation_expires_at()? {
438 candidates.push(instant);
439 }
440 candidates
441 .into_iter()
442 .min()
443 .ok_or(SamlError::TimeWindowInvalid {
444 field: REPLAY_EXPIRATION_FIELD,
445 })
446 }
447
448 fn assertion_replay_instant(&self) -> Result<OffsetDateTime, SamlError> {
449 if let Some(instant) = self.bearer_subject_confirmation_expires_at()? {
450 return Ok(instant);
451 }
452 self.earliest_replay_instant()
453 }
454
455 fn bearer_subject_confirmation_expires_at(&self) -> Result<Option<OffsetDateTime>, SamlError> {
456 let fields = [
457 ExtractorField::new("subjectConfirmation", &["SubjectConfirmation"]).attrs(&["Method"]),
458 ExtractorField::new(
459 "subjectConfirmationData",
460 &["SubjectConfirmation", "SubjectConfirmationData"],
461 )
462 .attrs(&["NotOnOrAfter"]),
463 ];
464 let mut expires_at = None;
465 for confirmation in self.subject.confirmations() {
466 let extracted =
467 extract_with_limits(confirmation.raw_xml(), &fields, XmlLimits::default())?;
468 if extracted.get_str("subjectConfirmation") != Some(BEARER_SUBJECT_CONFIRMATION_METHOD)
469 {
470 continue;
471 }
472 let Some(not_on_or_after) = extracted.get_str("subjectConfirmationData") else {
473 continue;
474 };
475 let candidate = parse_replay_expiration(not_on_or_after)?;
476 match expires_at {
477 Some(current) if current >= candidate => {}
478 Some(_) | None => expires_at = Some(candidate),
479 }
480 }
481 Ok(expires_at)
482 }
483}
484
485fn replay_deadline(
486 raw_expires_at: OffsetDateTime,
487 validation_now: OffsetDateTime,
488 not_on_or_after_skew_ms: i64,
489) -> Result<OffsetDateTime, SamlError> {
490 let expires_at = raw_expires_at
491 .checked_add(Duration::milliseconds(not_on_or_after_skew_ms))
492 .ok_or(SamlError::TimeWindowInvalid {
493 field: REPLAY_EXPIRATION_FIELD,
494 })?;
495 if validation_now >= expires_at {
496 return Err(SamlError::TimeWindowInvalid {
497 field: REPLAY_EXPIRATION_FIELD,
498 });
499 }
500 Ok(expires_at)
501}
502
503fn system_time_from_offset(expires_at: OffsetDateTime) -> Result<SystemTime, SamlError> {
504 let since_epoch = expires_at - OffsetDateTime::UNIX_EPOCH;
505 let converted = if since_epoch.is_negative() {
506 SystemTime::UNIX_EPOCH.checked_sub(since_epoch.unsigned_abs())
507 } else {
508 SystemTime::UNIX_EPOCH.checked_add(since_epoch.unsigned_abs())
509 };
510 converted.ok_or(SamlError::TimeWindowInvalid {
511 field: REPLAY_EXPIRATION_FIELD,
512 })
513}
514
515fn parse_replay_expiration(value: &str) -> Result<OffsetDateTime, SamlError> {
516 OffsetDateTime::parse(value, &Rfc3339).map_err(|_| SamlError::TimeWindowInvalid {
517 field: REPLAY_EXPIRATION_FIELD,
518 })
519}
520
521impl TryFrom<FlowResult> for SsoSession {
522 type Error = SamlError;
523
524 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
525 Self::try_from_with_verified_xml_signatures(raw_flow, Vec::new())
526 }
527}
528
529impl SsoSession {
530 pub(crate) fn try_from_with_verified_xml_signatures(
531 raw_flow: FlowResult,
532 verified_xml_signatures: Vec<VerifiedXmlSignature>,
533 ) -> Result<Self, SamlError> {
534 let response_id = MessageId::try_new(required_str(&raw_flow.extract, "response.id")?)?;
535 let response_issue_instant =
536 issue_instant_from_extract(&raw_flow.extract, "response.issueInstant", "Response")?;
537 let assertion_id = AssertionId::try_new(required_str(&raw_flow.extract, "assertion.id")?)?;
538 let assertion_issue_instant =
539 issue_instant_from_extract(&raw_flow.extract, "assertion.issueInstant", "Assertion")?;
540 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
541 let in_response_to = optional_request_id(&raw_flow.extract, "response.inResponseTo")?;
542 let name_id_format = raw_flow
543 .extract
544 .get_str("nameIDFormat")
545 .map(name_id_format_from_uri);
546 let name_id = NameId::new(required_str(&raw_flow.extract, "nameID")?, name_id_format);
547 let subject = Subject::new(
548 name_id,
549 subject_confirmations_from_extract(&raw_flow.extract),
550 );
551 let attributes = attributes_from_extract(&raw_flow.extract);
552 let authn_sessions = authn_sessions_from_extract(&raw_flow.extract)?;
553 let audience = entity_ids_from_value(raw_flow.extract.get("audience"))?;
554 let (not_before, not_on_or_after) = conditions_instants(&raw_flow.extract)?;
555 let sig_alg = raw_flow.sig_alg.clone();
556 Ok(Self {
557 response_id,
558 response_issue_instant,
559 assertion_id,
560 assertion_issue_instant,
561 issuer,
562 in_response_to,
563 subject,
564 attributes,
565 authn_sessions,
566 audience,
567 not_before,
568 not_on_or_after,
569 sig_alg,
570 verified_xml_signatures,
571 raw_flow,
572 })
573 }
574}
575
576fn issue_instant_from_extract(
577 extract: &crate::util::Value,
578 path: &str,
579 element: &str,
580) -> Result<SamlInstant, SamlError> {
581 let issue_instant = extract.get_str(path).ok_or_else(|| {
582 SamlError::ProtocolProfile(format!(
583 "{element} is missing required unqualified attribute IssueInstant"
584 ))
585 })?;
586 let issue_instant = parse_saml_utc_date_time(issue_instant).ok_or_else(|| {
587 SamlError::ProtocolProfile(format!(
588 "{element} IssueInstant must use the SAML-conformant UTC xs:dateTime form ending in Z"
589 ))
590 })?;
591 SamlInstant::try_new(issue_instant)
592}