use chrono::{DateTime, Utc};
use crate::core::assertion::types::Assertion;
use crate::core::constants::{CM_BEARER, NAMEID_ENTITY, NAMEID_PERSISTENT};
use crate::core::protocol::response::Response;
use crate::security::audience::evaluate_audience_restrictions;
use crate::security::clock::{is_not_before_valid, is_not_on_or_after_valid, is_within_age_limit};
use crate::security::conditions::{check_one_time_use, check_proxy_restriction};
use crate::security::config::SecurityConfig;
use crate::security::destination::verify_destination;
use crate::security::error::{ValidationCheck, ValidationResult};
use crate::security::name_id::PersistentIdStore;
use crate::security::recipient::verify_recipient;
use crate::security::relay_state::validate_relay_state_content;
use crate::security::replay::ReplayCache;
use crate::security::signature::contains_ds_object;
pub struct ValidationParams<'a> {
pub received_url: &'a str,
pub expected_idp_entity_id: &'a str,
pub sp_entity_id: &'a str,
pub acs_url: &'a str,
pub expected_request_id: Option<&'a str>,
pub client_address: Option<&'a str>,
pub relay_state: Option<&'a str>,
pub response_signature_xml: Option<&'a str>,
pub response_signature_verified: Option<bool>,
pub current_proxy_depth: u32,
pub now: DateTime<Utc>,
}
pub struct AssertionValidator<'a> {
config: &'a SecurityConfig,
replay_cache: Option<&'a dyn ReplayCache>,
persistent_id_store: Option<&'a dyn PersistentIdStore>,
}
impl<'a> AssertionValidator<'a> {
pub fn new(config: &'a SecurityConfig) -> Self {
Self {
config,
replay_cache: None,
persistent_id_store: None,
}
}
pub fn with_replay_cache(mut self, cache: &'a dyn ReplayCache) -> Self {
self.replay_cache = Some(cache);
self
}
pub fn with_persistent_id_store(mut self, store: &'a dyn PersistentIdStore) -> Self {
self.persistent_id_store = Some(store);
self
}
pub fn validate_response(
&self,
response: &Response,
params: &ValidationParams<'_>,
) -> ValidationResult {
let mut result = ValidationResult::new();
self.check_response_level(response, params, &mut result);
for assertion in &response.assertions {
self.check_assertion_level(assertion, params, &mut result);
}
if let Some(relay_state) = params.relay_state {
self.check_relay_state(relay_state, &mut result);
}
result
}
fn check_response_level(
&self,
response: &Response,
params: &ValidationParams<'_>,
result: &mut ValidationResult,
) {
let is_signed = response.base.has_signature;
if self.config.verify_destination {
match verify_destination(
response.base.destination.as_deref(),
params.received_url,
is_signed,
) {
Ok(()) => result.add(ValidationCheck::pass(1, "Destination matches URL")),
Err(e) => result.add(ValidationCheck::fail(1, "Destination matches URL", e)),
}
} else {
result.add(ValidationCheck::pass(
1,
"Destination matches URL (skipped)",
));
}
if let Some(ref issuer) = response.base.issuer {
if let Some(ref format) = issuer.format {
if format == NAMEID_ENTITY {
result.add(ValidationCheck::pass(2, "Issuer format valid"));
} else {
result.add(ValidationCheck::fail(
2,
"Issuer format valid",
format!("Issuer format must be entity or omitted, got '{}'", format),
));
}
} else {
result.add(ValidationCheck::pass(2, "Issuer format valid"));
}
} else {
result.add(ValidationCheck::pass(2, "Issuer format valid"));
}
match (
response.base.in_response_to.as_deref(),
params.expected_request_id,
) {
(Some(irt), Some(expected)) => {
if irt == expected {
result.add(ValidationCheck::pass(3, "InResponseTo matches"));
} else {
result.add(ValidationCheck::fail(
3,
"InResponseTo matches",
format!("Expected '{}', got '{}'", expected, irt),
));
}
}
(None, None) => {
result.add(ValidationCheck::pass(3, "InResponseTo matches"));
}
(Some(_), None) => {
result.add(ValidationCheck::pass(3, "InResponseTo matches"));
}
(None, Some(expected)) => {
result.add(ValidationCheck::fail(
3,
"InResponseTo matches",
format!("Expected InResponseTo='{}' but none present", expected),
));
}
}
if is_signed {
match params.response_signature_verified {
Some(true) => {
result.add(ValidationCheck::pass(4, "Response signature valid"));
}
Some(false) => {
result.add(ValidationCheck::fail(
4,
"Response signature valid",
"Response signature verification failed",
));
}
None => {
result.add(ValidationCheck::fail(
4,
"Response signature valid",
"Response has signature but verification was not performed",
));
}
}
} else if self.config.require_signed_responses {
result.add(ValidationCheck::fail(
4,
"Response signature valid",
"Response signature required but not present",
));
} else {
result.add(ValidationCheck::pass(4, "Response signature valid"));
}
if let Some(sig_xml) = params.response_signature_xml {
if self.config.reject_signatures_with_ds_object && contains_ds_object(sig_xml) {
result.add(ValidationCheck::fail(
7,
"No ds:Object in signature",
"Response signature contains ds:Object (E91)",
));
}
}
}
fn check_assertion_level(
&self,
assertion: &Assertion,
params: &ValidationParams<'_>,
result: &mut ValidationResult,
) {
if assertion.issuer.value == params.expected_idp_entity_id {
result.add(ValidationCheck::pass(5, "Issuer matches IdP"));
} else {
result.add(ValidationCheck::fail(
5,
"Issuer matches IdP",
format!(
"Expected '{}', got '{}'",
params.expected_idp_entity_id, assertion.issuer.value
),
));
}
if assertion.has_signature {
result.add(ValidationCheck::pass(6, "Assertion signature present"));
} else if self.config.require_signed_assertions {
result.add(ValidationCheck::fail(
6,
"Assertion signature present",
"Assertion signature required but not present",
));
} else {
result.add(ValidationCheck::pass(6, "Assertion signature present"));
}
if !result.checks.iter().any(|c| c.check_number == 7) {
result.add(ValidationCheck::pass(7, "No ds:Object in signature"));
}
result.add(ValidationCheck::pass(8, "Signature algorithm supported"));
if let Some(ref conditions) = assertion.conditions {
if let Some(not_before) = conditions.not_before {
if is_not_before_valid(params.now, not_before, self.config.clock_skew_seconds) {
result.add(ValidationCheck::pass(9, "NotBefore valid"));
} else {
result.add(ValidationCheck::fail(
9,
"NotBefore valid",
format!(
"Now {} is before NotBefore {} (skew: {}s)",
params.now, not_before, self.config.clock_skew_seconds
),
));
}
} else {
result.add(ValidationCheck::pass(9, "NotBefore valid"));
}
if let Some(not_on_or_after) = conditions.not_on_or_after {
if is_not_on_or_after_valid(
params.now,
not_on_or_after,
self.config.clock_skew_seconds,
) {
result.add(ValidationCheck::pass(10, "NotOnOrAfter valid"));
} else {
result.add(ValidationCheck::fail(
10,
"NotOnOrAfter valid",
format!(
"Now {} is at/after NotOnOrAfter {} (skew: {}s)",
params.now, not_on_or_after, self.config.clock_skew_seconds
),
));
}
} else {
result.add(ValidationCheck::pass(10, "NotOnOrAfter valid"));
}
if evaluate_audience_restrictions(
&conditions.audience_restrictions,
params.sp_entity_id,
) {
result.add(ValidationCheck::pass(11, "AudienceRestriction satisfied"));
} else {
result.add(ValidationCheck::fail(
11,
"AudienceRestriction satisfied",
format!(
"SP '{}' is not in any audience restriction",
params.sp_entity_id
),
));
}
result.add(check_one_time_use(conditions.one_time_use));
let proxy_limit = conditions
.proxy_restriction
.as_ref()
.and_then(|pr| pr.count);
result.add(check_proxy_restriction(
proxy_limit,
params.current_proxy_depth,
));
} else {
result.add(ValidationCheck::pass(9, "NotBefore valid"));
result.add(ValidationCheck::pass(10, "NotOnOrAfter valid"));
result.add(ValidationCheck::pass(11, "AudienceRestriction satisfied"));
result.add(ValidationCheck::pass(12, "OneTimeUse condition"));
result.add(ValidationCheck::pass(13, "ProxyRestriction count"));
}
self.check_subject_confirmation(assertion, params, result);
self.check_replay(assertion, params, result);
self.check_authn_statements(assertion, params, result);
result.add(ValidationCheck::pass(24, "CBC integrity check"));
self.check_name_id(assertion, params, result);
}
fn check_subject_confirmation(
&self,
assertion: &Assertion,
params: &ValidationParams<'_>,
result: &mut ValidationResult,
) {
let subject = match &assertion.subject {
Some(s) => s,
None => {
result.add(ValidationCheck::fail(
14,
"Bearer confirmation method",
"No Subject element in assertion",
));
return;
}
};
let bearer_confirmation = subject
.subject_confirmations
.iter()
.find(|sc| sc.method == CM_BEARER);
let sc = match bearer_confirmation {
Some(sc) => sc,
None => {
result.add(ValidationCheck::fail(
14,
"Bearer confirmation method",
"No bearer SubjectConfirmation found",
));
return;
}
};
result.add(ValidationCheck::pass(14, "Bearer confirmation method"));
let scd = match &sc.subject_confirmation_data {
Some(d) => d,
None => {
result.add(ValidationCheck::fail(
15,
"Recipient matches ACS URL",
"No SubjectConfirmationData in bearer confirmation",
));
return;
}
};
if self.config.verify_recipient {
match verify_recipient(scd.recipient.as_deref(), params.acs_url) {
Ok(()) => result.add(ValidationCheck::pass(15, "Recipient matches ACS URL")),
Err(e) => result.add(ValidationCheck::fail(15, "Recipient matches ACS URL", e)),
}
} else {
result.add(ValidationCheck::pass(
15,
"Recipient matches ACS URL (skipped)",
));
}
if let Some(not_on_or_after) = scd.not_on_or_after {
if is_not_on_or_after_valid(params.now, not_on_or_after, self.config.clock_skew_seconds)
{
result.add(ValidationCheck::pass(
16,
"SubjectConfirmation NotOnOrAfter",
));
} else {
result.add(ValidationCheck::fail(
16,
"SubjectConfirmation NotOnOrAfter",
format!(
"SubjectConfirmationData NotOnOrAfter {} has passed (skew: {}s)",
not_on_or_after, self.config.clock_skew_seconds
),
));
}
} else {
result.add(ValidationCheck::pass(
16,
"SubjectConfirmation NotOnOrAfter",
));
}
match (scd.in_response_to.as_deref(), params.expected_request_id) {
(Some(irt), Some(expected)) => {
if irt == expected {
result.add(ValidationCheck::pass(
17,
"SubjectConfirmation InResponseTo",
));
} else {
result.add(ValidationCheck::fail(
17,
"SubjectConfirmation InResponseTo",
format!("Expected '{}', got '{}'", expected, irt),
));
}
}
(None, None) => {
result.add(ValidationCheck::pass(
17,
"SubjectConfirmation InResponseTo",
));
}
(Some(_), None) => {
result.add(ValidationCheck::pass(
17,
"SubjectConfirmation InResponseTo",
));
}
(None, Some(expected)) => {
result.add(ValidationCheck::fail(
17,
"SubjectConfirmation InResponseTo",
format!("Expected InResponseTo='{}' but none present", expected),
));
}
}
if scd.not_before.is_some() {
result.add(ValidationCheck::fail(
18,
"Bearer NotBefore absent",
"NotBefore MUST NOT be present in bearer SubjectConfirmationData",
));
} else {
result.add(ValidationCheck::pass(18, "Bearer NotBefore absent"));
}
if self.config.check_client_address {
match (&scd.address, params.client_address) {
(Some(expected_addr), Some(actual_addr)) => {
if expected_addr == actual_addr {
result.add(ValidationCheck::pass(19, "Client address matches"));
} else {
result.add(ValidationCheck::fail(
19,
"Client address matches",
format!(
"Expected address '{}', got '{}'",
expected_addr, actual_addr
),
));
}
}
(Some(_expected_addr), None) => {
result.add(ValidationCheck::fail(
19,
"Client address matches",
"SubjectConfirmationData has Address but client address is unknown",
));
}
(None, _) => {
result.add(ValidationCheck::pass(19, "Client address matches"));
}
}
} else {
result.add(ValidationCheck::pass(
19,
"Client address matches (skipped)",
));
}
}
fn check_replay(
&self,
assertion: &Assertion,
params: &ValidationParams<'_>,
result: &mut ValidationResult,
) {
if let Some(cache) = self.replay_cache {
let expiry = assertion
.conditions
.as_ref()
.and_then(|c| c.not_on_or_after)
.unwrap_or_else(|| {
params.now
+ chrono::TimeDelta::seconds(self.config.max_assertion_age_seconds as i64)
});
if cache.check_and_insert(&assertion.id, expiry) {
result.add(ValidationCheck::pass(20, "Assertion ID not replayed"));
} else {
result.add(ValidationCheck::fail(
20,
"Assertion ID not replayed",
format!("Assertion ID '{}' was previously used", assertion.id),
));
}
} else {
result.add(ValidationCheck::pass(
20,
"Assertion ID not replayed (no cache)",
));
}
}
fn check_authn_statements(
&self,
assertion: &Assertion,
params: &ValidationParams<'_>,
result: &mut ValidationResult,
) {
if assertion.authn_statements.is_empty() {
result.add(ValidationCheck::fail(
21,
"AuthnStatement present",
"No AuthnStatement in assertion",
));
return;
}
result.add(ValidationCheck::pass(21, "AuthnStatement present"));
let has_session_index = assertion
.authn_statements
.iter()
.any(|s| s.session_index.is_some());
if has_session_index {
result.add(ValidationCheck::pass(22, "SessionIndex present"));
} else {
result.add(ValidationCheck::pass(22, "SessionIndex present (optional)"));
}
for stmt in &assertion.authn_statements {
if let Some(session_not_on_or_after) = stmt.session_not_on_or_after {
if is_not_on_or_after_valid(
params.now,
session_not_on_or_after,
self.config.clock_skew_seconds,
) {
result.add(ValidationCheck::pass(23, "SessionNotOnOrAfter valid"));
} else {
result.add(ValidationCheck::fail(
23,
"SessionNotOnOrAfter valid",
format!(
"Session expired at {} (E79: upper bound)",
session_not_on_or_after
),
));
}
} else {
result.add(ValidationCheck::pass(23, "SessionNotOnOrAfter valid"));
}
}
}
fn check_name_id(
&self,
assertion: &Assertion,
params: &ValidationParams<'_>,
result: &mut ValidationResult,
) {
let subject = match &assertion.subject {
Some(s) => s,
None => {
result.add(ValidationCheck::pass(25, "NameIDPolicy Format"));
result.add(ValidationCheck::pass(26, "Persistent ID unique"));
result.add(ValidationCheck::pass(27, "AllowCreate semantics"));
return;
}
};
result.add(ValidationCheck::pass(25, "NameIDPolicy Format"));
if self.config.enforce_persistent_id_uniqueness {
if let Some(ref name_id) = subject.name_id {
match name_id {
crate::core::assertion::name_id::NameIdOrEncryptedId::NameId(nid) => {
if nid.format.as_deref() == Some(NAMEID_PERSISTENT) {
if let Some(store) = self.persistent_id_store {
match store.check_and_record(
&nid.value,
params.sp_entity_id,
&nid.value, ) {
Ok(()) => {
result
.add(ValidationCheck::pass(26, "Persistent ID unique"));
}
Err(e) => {
result.add(ValidationCheck::fail(
26,
"Persistent ID unique",
e,
));
}
}
} else {
result.add(ValidationCheck::pass(
26,
"Persistent ID unique (no store)",
));
}
} else {
result.add(ValidationCheck::pass(26, "Persistent ID unique"));
}
}
crate::core::assertion::name_id::NameIdOrEncryptedId::EncryptedId(_) => {
result.add(ValidationCheck::pass(
26,
"Persistent ID unique (encrypted)",
));
}
}
} else {
result.add(ValidationCheck::pass(26, "Persistent ID unique"));
}
} else {
result.add(ValidationCheck::pass(26, "Persistent ID unique (skipped)"));
}
result.add(ValidationCheck::pass(27, "AllowCreate semantics"));
}
fn check_relay_state(&self, relay_state: &str, result: &mut ValidationResult) {
if relay_state.len() <= crate::security::relay_state::MAX_RELAY_STATE_BYTES {
result.add(ValidationCheck::pass(31, "RelayState length"));
} else {
result.add(ValidationCheck::fail(
31,
"RelayState length",
format!(
"RelayState is {} bytes, exceeds 80-byte limit",
relay_state.len()
),
));
}
if self.config.sanitize_relay_state {
match validate_relay_state_content(relay_state) {
Ok(()) => result.add(ValidationCheck::pass(32, "RelayState sanitized")),
Err(e) => result.add(ValidationCheck::fail(32, "RelayState sanitized", e)),
}
} else {
result.add(ValidationCheck::pass(32, "RelayState sanitized (skipped)"));
}
}
pub fn validate_response_simple(
&self,
response: &Response,
params: &ValidationParams<'_>,
) -> Result<(), Vec<ValidationCheck>> {
let result = self.validate_response(response, params);
if result.is_valid() {
Ok(())
} else {
Err(result.failures().into_iter().cloned().collect())
}
}
pub fn check_assertion_age(
&self,
issue_instant: DateTime<Utc>,
now: DateTime<Utc>,
) -> ValidationCheck {
if is_within_age_limit(now, issue_instant, self.config.max_assertion_age_seconds) {
ValidationCheck::pass(0, "Assertion age within limit")
} else {
ValidationCheck::fail(
0,
"Assertion age within limit",
format!(
"Assertion issued at {} exceeds max age of {}s",
issue_instant, self.config.max_assertion_age_seconds
),
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::assertion::authn::{AuthnContext, AuthnStatement};
use crate::core::assertion::conditions::{AudienceRestriction, Conditions};
use crate::core::assertion::issuer::Issuer;
use crate::core::assertion::name_id::{NameId, NameIdOrEncryptedId};
use crate::core::assertion::subject::{Subject, SubjectConfirmation, SubjectConfirmationData};
use crate::core::constants::*;
use crate::core::identifiers::SamlVersion;
use crate::core::protocol::response::{Response, ResponseBase};
use crate::core::protocol::status::Status;
use crate::security::replay::InMemoryReplayCache;
use chrono::TimeDelta;
fn make_valid_response(now: chrono::DateTime<Utc>) -> Response {
Response {
base: ResponseBase {
id: "_resp_123".to_string(),
version: SamlVersion::V2_0,
issue_instant: now,
destination: Some("https://sp.example.com/acs".to_string()),
consent: None,
issuer: Some(Issuer::entity("https://idp.example.com")),
has_signature: false,
in_response_to: Some("_req_456".to_string()),
status: Status::success(),
},
assertions: vec![Assertion {
id: "_assertion_789".to_string(),
issue_instant: now,
version: SamlVersion::V2_0,
issuer: Issuer::entity("https://idp.example.com"),
has_signature: false,
subject: Some(Subject {
name_id: Some(NameIdOrEncryptedId::NameId(NameId {
value: "user@example.com".to_string(),
format: Some(NAMEID_EMAIL.to_string()),
name_qualifier: None,
sp_name_qualifier: None,
sp_provided_id: None,
})),
subject_confirmations: vec![SubjectConfirmation {
method: CM_BEARER.to_string(),
name_id: None,
subject_confirmation_data: Some(SubjectConfirmationData {
not_before: None,
not_on_or_after: Some(now + TimeDelta::seconds(300)),
recipient: Some("https://sp.example.com/acs".to_string()),
in_response_to: Some("_req_456".to_string()),
address: None,
key_info_x509_certs: vec![],
}),
}],
}),
conditions: Some(Conditions {
not_before: Some(now - TimeDelta::seconds(60)),
not_on_or_after: Some(now + TimeDelta::seconds(300)),
audience_restrictions: vec![AudienceRestriction {
audiences: vec!["https://sp.example.com".to_string()],
}],
one_time_use: false,
proxy_restriction: None,
}),
advice: None,
authn_statements: vec![AuthnStatement {
authn_instant: now,
session_index: Some("_session_001".to_string()),
session_not_on_or_after: Some(now + TimeDelta::seconds(3600)),
subject_locality: None,
authn_context: AuthnContext {
authn_context_class_ref: Some(
AUTHN_CONTEXT_PASSWORD_PROTECTED_TRANSPORT.to_string(),
),
authn_context_decl_ref: None,
authenticating_authorities: vec![],
},
}],
authz_decision_statements: vec![],
attribute_statements: vec![],
}],
encrypted_assertions: vec![],
}
}
fn make_params(now: chrono::DateTime<Utc>) -> ValidationParams<'static> {
ValidationParams {
received_url: "https://sp.example.com/acs",
expected_idp_entity_id: "https://idp.example.com",
sp_entity_id: "https://sp.example.com",
acs_url: "https://sp.example.com/acs",
expected_request_id: Some("_req_456"),
client_address: None,
relay_state: None,
response_signature_xml: None,
response_signature_verified: None,
current_proxy_depth: 0,
now,
}
}
#[test]
fn test_valid_response_passes_all_checks() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let response = make_valid_response(now);
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(
failures.is_empty(),
"Expected no failures, got: {:?}",
failures
);
}
#[test]
fn test_destination_mismatch() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.base.destination = Some("https://evil.com/acs".to_string());
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 1));
}
#[test]
fn test_issuer_mismatch() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0].issuer = Issuer::entity("https://evil-idp.com");
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 5));
}
#[test]
fn test_audience_restriction_failed() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0]
.conditions
.as_mut()
.unwrap()
.audience_restrictions = vec![AudienceRestriction {
audiences: vec!["https://other-sp.com".to_string()],
}];
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 11));
}
#[test]
fn test_expired_assertion() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0]
.conditions
.as_mut()
.unwrap()
.not_on_or_after = Some(now - TimeDelta::seconds(600));
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 10));
}
#[test]
fn test_replay_detection() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let cache = InMemoryReplayCache::new();
let validator = AssertionValidator::new(&config).with_replay_cache(&cache);
let response = make_valid_response(now);
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
assert!(result.is_valid());
let result2 = validator.validate_response(&response, ¶ms);
let failures = result2.failures();
assert!(failures.iter().any(|c| c.check_number == 20));
}
#[test]
fn test_bearer_not_before_present() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0]
.subject
.as_mut()
.unwrap()
.subject_confirmations[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.not_before = Some(now - TimeDelta::seconds(60));
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 18));
}
#[test]
fn test_in_response_to_mismatch() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.base.in_response_to = Some("_wrong_id".to_string());
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 3));
}
#[test]
fn test_no_authn_statement() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0].authn_statements.clear();
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 21));
}
#[test]
fn test_relay_state_too_long() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let response = make_valid_response(now);
let relay_state: &'static str = Box::leak("a".repeat(100).into_boxed_str());
let params = ValidationParams {
relay_state: Some(relay_state),
..make_params(now)
};
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 31));
}
#[test]
fn test_relay_state_xss() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let response = make_valid_response(now);
let relay_state: &'static str =
Box::leak("javascript:alert(1)".to_string().into_boxed_str());
let params = ValidationParams {
relay_state: Some(relay_state),
..make_params(now)
};
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 32));
}
#[test]
fn test_recipient_mismatch() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0]
.subject
.as_mut()
.unwrap()
.subject_confirmations[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.recipient = Some("https://evil.com/acs".to_string());
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 15));
}
#[test]
fn test_session_expired() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0].authn_statements[0].session_not_on_or_after =
Some(now - TimeDelta::seconds(600));
let params = make_params(now);
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 23));
}
#[test]
fn test_validate_response_simple_ok() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let response = make_valid_response(now);
let params = make_params(now);
assert!(validator
.validate_response_simple(&response, ¶ms)
.is_ok());
}
#[test]
fn test_validate_response_simple_err() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.base.destination = Some("https://evil.com".to_string());
let params = make_params(now);
let err = validator
.validate_response_simple(&response, ¶ms)
.unwrap_err();
assert!(!err.is_empty());
}
#[test]
fn test_unsolicited_response() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.base.in_response_to = None;
response.assertions[0]
.subject
.as_mut()
.unwrap()
.subject_confirmations[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.in_response_to = None;
let params = ValidationParams {
expected_request_id: None,
..make_params(now)
};
let result = validator.validate_response(&response, ¶ms);
assert!(result.is_valid(), "Failures: {:?}", result.failures());
}
#[test]
fn test_address_check_enabled() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
check_client_address: true,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.assertions[0]
.subject
.as_mut()
.unwrap()
.subject_confirmations[0]
.subject_confirmation_data
.as_mut()
.unwrap()
.address = Some("10.0.0.1".to_string());
let params = ValidationParams {
client_address: Some("10.0.0.2"),
..make_params(now)
};
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 19));
}
#[test]
fn test_ds_object_in_response_signature() {
let now = Utc::now();
let config = SecurityConfig {
require_signed_assertions: false,
..SecurityConfig::default()
};
let validator = AssertionValidator::new(&config);
let mut response = make_valid_response(now);
response.base.has_signature = true;
let sig_xml = r#"<ds:Signature><ds:Object>evil</ds:Object></ds:Signature>"#;
let params = ValidationParams {
response_signature_xml: Some(sig_xml),
response_signature_verified: Some(true),
..make_params(now)
};
let result = validator.validate_response(&response, ¶ms);
let failures = result.failures();
assert!(failures.iter().any(|c| c.check_number == 7));
}
#[test]
fn test_assertion_age_check() {
let now = Utc::now();
let config = SecurityConfig::default();
let validator = AssertionValidator::new(&config);
let recent = now - TimeDelta::seconds(60);
assert!(validator.check_assertion_age(recent, now).passed);
let old = now - TimeDelta::seconds(600);
assert!(!validator.check_assertion_age(old, now).passed);
}
}