saml_rs/model/
outstanding_logout.rs1use super::identifiers::{SamlInstant, SessionIndex};
2use super::sso::SsoSession;
3use super::subject::NameId;
4use super::validation::SamlValidationContext;
5use crate::error::SamlError;
6use crate::validator::effective_not_on_or_after;
7use crate::xml::parse_saml_utc_date_time;
8use time::{format_description::well_known::Rfc3339, OffsetDateTime};
9
10#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct OutstandingLogout {
41 principal: NameId,
42 sessions: Vec<SessionIndex>,
43 not_on_or_after: SamlInstant,
44 deadline: OffsetDateTime,
45}
46
47impl OutstandingLogout {
48 pub fn try_new(
56 principal: NameId,
57 sessions: Vec<SessionIndex>,
58 not_on_or_after: SamlInstant,
59 ) -> Result<Self, SamlError> {
60 if principal.value().trim().is_empty() {
61 return Err(SamlError::Invalid(
62 "outstanding logout principal must not be empty".into(),
63 ));
64 }
65 let normalized = parse_saml_utc_date_time(not_on_or_after.as_str()).ok_or_else(|| {
66 SamlError::Invalid(
67 "outstanding logout NotOnOrAfter must be a SAML UTC xs:dateTime ending in Z".into(),
68 )
69 })?;
70 let deadline = OffsetDateTime::parse(normalized, &Rfc3339).map_err(|_| {
71 SamlError::Invalid(
72 "outstanding logout NotOnOrAfter cannot be represented as a UTC instant".into(),
73 )
74 })?;
75 let not_on_or_after = SamlInstant::try_new(normalized)?;
76 Ok(Self {
77 principal,
78 sessions,
79 not_on_or_after,
80 deadline,
81 })
82 }
83
84 pub fn principal(&self) -> &NameId {
86 &self.principal
87 }
88
89 pub fn sessions(&self) -> &[SessionIndex] {
93 &self.sessions
94 }
95
96 pub fn not_on_or_after(&self) -> &SamlInstant {
98 &self.not_on_or_after
99 }
100
101 pub(crate) fn matches_unexpired(
102 &self,
103 session: &SsoSession,
104 validation: &SamlValidationContext<'_>,
105 ) -> Result<bool, SamlError> {
106 if !self.same_principal(session.name_id()) || !self.same_session(session) {
107 return Ok(false);
108 }
109 let now = validation.now_offset()?;
110 let effective = effective_not_on_or_after(
111 self.deadline,
112 validation.clock_skew().not_on_or_after_millis(),
113 )?;
114 Ok(now < effective)
115 }
116
117 fn same_principal(&self, name_id: &NameId) -> bool {
118 if self.principal.value() != name_id.value() {
119 return false;
120 }
121 self.principal
122 .format()
123 .is_none_or(|format| name_id.format() == Some(format))
124 }
125
126 fn same_session(&self, session: &SsoSession) -> bool {
127 if self.sessions.is_empty() {
128 return true;
129 }
130 session.authn_sessions().iter().any(|authn| {
131 authn
132 .session_index()
133 .is_some_and(|index| self.sessions.contains(index))
134 })
135 }
136}