use super::identifiers::{SamlInstant, SessionIndex};
use super::sso::SsoSession;
use super::subject::NameId;
use super::validation::SamlValidationContext;
use crate::error::SamlError;
use crate::validator::effective_not_on_or_after;
use crate::xml::parse_saml_utc_date_time;
use time::{format_description::well_known::Rfc3339, OffsetDateTime};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OutstandingLogout {
principal: NameId,
sessions: Vec<SessionIndex>,
not_on_or_after: SamlInstant,
deadline: OffsetDateTime,
}
impl OutstandingLogout {
pub fn try_new(
principal: NameId,
sessions: Vec<SessionIndex>,
not_on_or_after: SamlInstant,
) -> Result<Self, SamlError> {
if principal.value().trim().is_empty() {
return Err(SamlError::Invalid(
"outstanding logout principal must not be empty".into(),
));
}
let normalized = parse_saml_utc_date_time(not_on_or_after.as_str()).ok_or_else(|| {
SamlError::Invalid(
"outstanding logout NotOnOrAfter must be a SAML UTC xs:dateTime ending in Z".into(),
)
})?;
let deadline = OffsetDateTime::parse(normalized, &Rfc3339).map_err(|_| {
SamlError::Invalid(
"outstanding logout NotOnOrAfter cannot be represented as a UTC instant".into(),
)
})?;
let not_on_or_after = SamlInstant::try_new(normalized)?;
Ok(Self {
principal,
sessions,
not_on_or_after,
deadline,
})
}
pub fn principal(&self) -> &NameId {
&self.principal
}
pub fn sessions(&self) -> &[SessionIndex] {
&self.sessions
}
pub fn not_on_or_after(&self) -> &SamlInstant {
&self.not_on_or_after
}
pub(crate) fn matches_unexpired(
&self,
session: &SsoSession,
validation: &SamlValidationContext<'_>,
) -> Result<bool, SamlError> {
if !self.same_principal(session.name_id()) || !self.same_session(session) {
return Ok(false);
}
let now = validation.now_offset()?;
let effective = effective_not_on_or_after(
self.deadline,
validation.clock_skew().not_on_or_after_millis(),
)?;
Ok(now < effective)
}
fn same_principal(&self, name_id: &NameId) -> bool {
if self.principal.value() != name_id.value() {
return false;
}
self.principal
.format()
.is_none_or(|format| name_id.format() == Some(format))
}
fn same_session(&self, session: &SsoSession) -> bool {
if self.sessions.is_empty() {
return true;
}
session.authn_sessions().iter().any(|authn| {
authn
.session_index()
.is_some_and(|index| self.sessions.contains(index))
})
}
}