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, 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 issuer: EntityId,
29 in_response_to: Option<MessageId>,
30 raw_flow: FlowResult,
31}
32
33impl SsoResponse {
34 pub fn response_id(&self) -> &MessageId {
36 &self.response_id
37 }
38
39 pub fn issuer(&self) -> &EntityId {
41 &self.issuer
42 }
43
44 pub fn in_response_to(&self) -> Option<&MessageId> {
46 self.in_response_to.as_ref()
47 }
48
49 pub fn raw_flow(&self) -> &FlowResult {
51 &self.raw_flow
52 }
53}
54
55impl TryFrom<FlowResult> for SsoResponse {
56 type Error = SamlError;
57
58 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
59 let response_id = MessageId::try_new(required_str(&raw_flow.extract, "response.id")?)?;
60 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
61 let in_response_to = optional_request_id(&raw_flow.extract, "response.inResponseTo")?;
62 Ok(Self {
63 response_id,
64 issuer,
65 in_response_to,
66 raw_flow,
67 })
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct Assertion {
74 id: Option<AssertionId>,
75 issuer: EntityId,
76 subject: Subject,
77}
78
79impl Assertion {
80 pub fn new(id: Option<AssertionId>, issuer: EntityId, subject: Subject) -> Self {
82 Self {
83 id,
84 issuer,
85 subject,
86 }
87 }
88
89 pub fn id(&self) -> Option<&AssertionId> {
91 self.id.as_ref()
92 }
93
94 pub fn issuer(&self) -> &EntityId {
96 &self.issuer
97 }
98
99 pub fn subject(&self) -> &Subject {
101 &self.subject
102 }
103}
104
105#[derive(Debug, Clone)]
107pub struct SsoSession {
108 response_id: MessageId,
109 assertion_id: AssertionId,
110 issuer: EntityId,
111 in_response_to: Option<MessageId>,
112 subject: Subject,
113 attributes: Attributes,
114 authn_sessions: Vec<AuthnSession>,
115 audience: Vec<EntityId>,
116 not_before: Option<SamlInstant>,
117 not_on_or_after: Option<SamlInstant>,
118 sig_alg: Option<String>,
119 raw_flow: FlowResult,
120}
121
122impl SsoSession {
123 pub fn response_id(&self) -> &MessageId {
125 &self.response_id
126 }
127
128 pub fn assertion_id(&self) -> &AssertionId {
130 &self.assertion_id
131 }
132
133 pub fn issuer(&self) -> &EntityId {
135 &self.issuer
136 }
137
138 pub fn in_response_to(&self) -> Option<&MessageId> {
140 self.in_response_to.as_ref()
141 }
142
143 pub fn subject(&self) -> &Subject {
145 &self.subject
146 }
147
148 pub fn name_id(&self) -> &NameId {
150 self.subject.name_id()
151 }
152
153 pub fn attributes(&self) -> &Attributes {
155 &self.attributes
156 }
157
158 pub fn authn_session(&self) -> &AuthnSession {
165 self.authn_sessions.first().unwrap_or(&EMPTY_AUTHN_SESSION)
166 }
167
168 pub fn authn_sessions(&self) -> &[AuthnSession] {
170 &self.authn_sessions
171 }
172
173 pub fn audience(&self) -> &[EntityId] {
175 &self.audience
176 }
177
178 pub fn not_before(&self) -> Option<&SamlInstant> {
180 self.not_before.as_ref()
181 }
182
183 pub fn not_on_or_after(&self) -> Option<&SamlInstant> {
185 self.not_on_or_after.as_ref()
186 }
187
188 pub fn sig_alg(&self) -> Option<&str> {
190 self.sig_alg.as_deref()
191 }
192
193 pub fn assertion(&self) -> Assertion {
195 Assertion::new(
196 Some(self.assertion_id.clone()),
197 self.issuer.clone(),
198 self.subject.clone(),
199 )
200 }
201
202 pub fn logout_subject(&self) -> Option<LogoutSubject> {
227 if self.name_id().value().trim().is_empty() {
228 return None;
229 }
230 let session_indexes = self
231 .authn_sessions
232 .iter()
233 .filter_map(AuthnSession::session_index)
234 .cloned()
235 .collect();
236 Some(LogoutSubject::new(self.name_id().clone(), session_indexes))
237 }
238
239 pub fn replay_keys(&self) -> Vec<ReplayKey> {
241 vec![
242 ReplayKey::ResponseId(self.response_id.clone()),
243 ReplayKey::AssertionId(self.assertion_id.clone()),
244 ]
245 }
246
247 pub fn check_and_store_replay(
263 &self,
264 validation: &mut SamlValidationContext<'_>,
265 ) -> Result<(), SamlError> {
266 let validation_now = validation.now_offset()?;
267 let not_on_or_after_skew_ms = validation.clock_skew().not_on_or_after_millis();
268 match validation.replay_policy() {
269 ReplayPolicy::DisabledForCompatibility => Ok(()),
270 ReplayPolicy::RequireCache(cache) => {
271 let expires_at = self.replay_expires_at(validation_now, not_on_or_after_skew_ms)?;
272 let since_epoch = expires_at - OffsetDateTime::UNIX_EPOCH;
273 let expires_at = if since_epoch.is_negative() {
274 SystemTime::UNIX_EPOCH.checked_sub(since_epoch.unsigned_abs())
275 } else {
276 SystemTime::UNIX_EPOCH.checked_add(since_epoch.unsigned_abs())
277 }
278 .ok_or(SamlError::TimeWindowInvalid {
279 field: REPLAY_EXPIRATION_FIELD,
280 })?;
281 let keys = self.replay_keys();
282 for key in keys {
283 cache.check_and_store(key, expires_at)?;
284 }
285 Ok(())
286 }
287 }
288 }
289
290 pub fn raw_flow(&self) -> &FlowResult {
292 &self.raw_flow
293 }
294
295 fn replay_expires_at(
296 &self,
297 validation_now: OffsetDateTime,
298 not_on_or_after_skew_ms: i64,
299 ) -> Result<OffsetDateTime, SamlError> {
300 let mut candidates = Vec::with_capacity(3);
301 if let Some(instant) = self.not_on_or_after() {
302 candidates.push(parse_replay_expiration(instant.as_str())?);
303 }
304 if let Some(instant) = earliest_authn_session_expiration(
305 self.authn_sessions
306 .iter()
307 .filter_map(AuthnSession::not_on_or_after)
308 .map(SamlInstant::as_str),
309 REPLAY_EXPIRATION_FIELD,
310 )? {
311 candidates.push(instant);
312 }
313 if let Some(instant) = self.bearer_subject_confirmation_expires_at()? {
314 candidates.push(instant);
315 }
316
317 let raw_expires_at = candidates
318 .into_iter()
319 .min()
320 .ok_or(SamlError::TimeWindowInvalid {
321 field: REPLAY_EXPIRATION_FIELD,
322 })?;
323 let expires_at = raw_expires_at
324 .checked_add(Duration::milliseconds(not_on_or_after_skew_ms))
325 .ok_or(SamlError::TimeWindowInvalid {
326 field: REPLAY_EXPIRATION_FIELD,
327 })?;
328 if validation_now >= expires_at {
329 return Err(SamlError::TimeWindowInvalid {
330 field: REPLAY_EXPIRATION_FIELD,
331 });
332 }
333 Ok(expires_at)
334 }
335
336 fn bearer_subject_confirmation_expires_at(&self) -> Result<Option<OffsetDateTime>, SamlError> {
337 let fields = [
338 ExtractorField::new("subjectConfirmation", &["SubjectConfirmation"]).attrs(&["Method"]),
339 ExtractorField::new(
340 "subjectConfirmationData",
341 &["SubjectConfirmation", "SubjectConfirmationData"],
342 )
343 .attrs(&["NotOnOrAfter"]),
344 ];
345 let mut expires_at = None;
346 for confirmation in self.subject.confirmations() {
347 let extracted =
348 extract_with_limits(confirmation.raw_xml(), &fields, XmlLimits::default())?;
349 if extracted.get_str("subjectConfirmation") != Some(BEARER_SUBJECT_CONFIRMATION_METHOD)
350 {
351 continue;
352 }
353 let Some(not_on_or_after) = extracted.get_str("subjectConfirmationData") else {
354 continue;
355 };
356 let candidate = parse_replay_expiration(not_on_or_after)?;
357 match expires_at {
358 Some(current) if current >= candidate => {}
359 Some(_) | None => expires_at = Some(candidate),
360 }
361 }
362 Ok(expires_at)
363 }
364}
365
366fn parse_replay_expiration(value: &str) -> Result<OffsetDateTime, SamlError> {
367 OffsetDateTime::parse(value, &Rfc3339).map_err(|_| SamlError::TimeWindowInvalid {
368 field: REPLAY_EXPIRATION_FIELD,
369 })
370}
371
372impl TryFrom<FlowResult> for SsoSession {
373 type Error = SamlError;
374
375 fn try_from(raw_flow: FlowResult) -> Result<Self, Self::Error> {
376 let response_id = MessageId::try_new(required_str(&raw_flow.extract, "response.id")?)?;
377 let assertion_id = AssertionId::try_new(required_str(&raw_flow.extract, "assertion.id")?)?;
378 let issuer = EntityId::try_new(required_str(&raw_flow.extract, "issuer")?)?;
379 let in_response_to = optional_request_id(&raw_flow.extract, "response.inResponseTo")?;
380 let name_id_format = raw_flow
381 .extract
382 .get_str("nameIDFormat")
383 .map(name_id_format_from_uri);
384 let name_id = NameId::new(required_str(&raw_flow.extract, "nameID")?, name_id_format);
385 let subject = Subject::new(
386 name_id,
387 subject_confirmations_from_extract(&raw_flow.extract),
388 );
389 let attributes = attributes_from_extract(&raw_flow.extract);
390 let authn_sessions = authn_sessions_from_extract(&raw_flow.extract)?;
391 let audience = entity_ids_from_value(raw_flow.extract.get("audience"))?;
392 let (not_before, not_on_or_after) = conditions_instants(&raw_flow.extract)?;
393 let sig_alg = raw_flow.sig_alg.clone();
394 Ok(Self {
395 response_id,
396 assertion_id,
397 issuer,
398 in_response_to,
399 subject,
400 attributes,
401 authn_sessions,
402 audience,
403 not_before,
404 not_on_or_after,
405 sig_alg,
406 raw_flow,
407 })
408 }
409}